mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-02 17:50:39 +00:00
Grid-mode tuning!
- Change scrolling behaviour to lock headers in place
- Enable filtering (hitting enter in the search bar) in grid-mode
- Little more top-margin for k8s (can have 3 topos) + taller rows.
- Trying out rank-color + node.relatives in the grid-mode
- First pass at selecting rows.
- Needs a bit more a fiddle, colors + click areas
- Store grid sort direction (asc/desc) in url state
- Simplify node selection to one method. (over-ride existing card)
- Remove clicking on name directly (links) to overlay new cards for now.
- Playing w/ grid-mode-toggle icons and labels
- Improves rendering in ff, change of shortcut keys for grid-mode-toggle
- Playing w/ clearer selection colors for grid-mode
- Slight change to selection-ui
- Fixes showNodeInTopology button visibility on the details-panel
- Was using an old heuristic. Table-mode allows you to open child cards
before the parent.
- Make it clear what the default sort is in tables
- E.g. always show a sorting caret
- Sort grid-mode columns, first meta then metrics
- dancing-nodes rememdy #1: pause updates onRowHover
- Splits relatives out into their own columns
- Take into account scrollbar width for grid-mode col header position
- Tooltips on table column headers
- grid-mode: fixes first column headers (proc/container/c-by-image)
- Disable pause-on-hover, too aggresive
- reduce label column width a bit (33pc -> 25pc) for big tables
- Filter grid-mode onSearchChange
- Rather than previous behaviour of waiting for an <enter>
- Show label_minor on pseudo nodes, that might not have much other info
- grid-mode: further reduce width of id column.
- Fixes go tests, properly moves parents into node-summary
- Fixes sorting of string columns w/ missing fields.
- E.g. uptime. Where -1e-10 > '3days' doesn't work.
This commit is contained in:
@@ -36,14 +36,23 @@ 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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function sortOrderChanged(newOrder) {
|
||||
return {
|
||||
type: ActionTypes.SORT_ORDER_CHANGED,
|
||||
newOrder
|
||||
export function sortOrderChanged(sortBy, sortedDesc) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.SORT_ORDER_CHANGED,
|
||||
sortBy, sortedDesc
|
||||
});
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -368,7 +368,6 @@ class NodesChart extends React.Component {
|
||||
|
||||
if (!this.props.noZoom && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
|
||||
zoomScale = zoomFactor;
|
||||
console.log(zoomScale);
|
||||
// saving in d3's behavior cache
|
||||
if (this.zoom) {
|
||||
this.zoom.scale(zoomFactor);
|
||||
@@ -413,7 +412,6 @@ function mapStateToProps(state) {
|
||||
return {
|
||||
adjacentNodes: getAdjacentNodes(state),
|
||||
forceRelayout: state.get('forceRelayout'),
|
||||
nodes: state.get('nodes').filter(node => !node.get('filtered')),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
topologyId: state.get('currentTopologyId'),
|
||||
topologyOptions: getActiveTopologyOptions(state)
|
||||
|
||||
@@ -1,50 +1,137 @@
|
||||
/* eslint react/jsx-no-bind: "off", no-multi-comp: "off" */
|
||||
|
||||
import React from 'react';
|
||||
import { Set as makeSet, List as makeList, Map as makeMap } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
import { List as makeList, Map as makeMap } from 'immutable';
|
||||
import NodeDetailsTable from '../components/node-details/node-details-table';
|
||||
import { enterNode, leaveNode } from '../actions/app-actions';
|
||||
import { clickNode, sortOrderChanged, clickPauseUpdate,
|
||||
clickResumeUpdate } from '../actions/app-actions';
|
||||
|
||||
import { getNodeColor } from '../utils/color-utils';
|
||||
|
||||
|
||||
const IGNORED_COLUMNS = ['docker_container_ports'];
|
||||
const IGNORED_COLUMNS = ['docker_container_ports', 'docker_container_id', 'docker_image_id',
|
||||
'docker_container_command', 'docker_container_networks'];
|
||||
|
||||
|
||||
function getColumns(nodes) {
|
||||
const allColumns = nodes.toList().flatMap(n => {
|
||||
const metrics = (n.get('metrics') || makeList())
|
||||
.map(m => makeMap({ id: m.get('id'), label: m.get('label') }));
|
||||
const metadata = (n.get('metadata') || makeList())
|
||||
.map(m => makeMap({ id: m.get('id'), label: m.get('label') }));
|
||||
return metadata.concat(metrics);
|
||||
});
|
||||
return makeSet(allColumns).filter(n => !IGNORED_COLUMNS.includes(n.get('id'))).toJS();
|
||||
const metricColumns = nodes
|
||||
.toList()
|
||||
.flatMap(n => {
|
||||
const metrics = (n.get('metrics') || makeList())
|
||||
.map(m => makeMap({ id: m.get('id'), label: m.get('label') }));
|
||||
return metrics;
|
||||
})
|
||||
.toSet()
|
||||
.toList()
|
||||
.sortBy(m => m.get('label'));
|
||||
|
||||
const metadataColumns = nodes
|
||||
.toList()
|
||||
.flatMap(n => {
|
||||
const metadata = (n.get('metadata') || makeList())
|
||||
.map(m => makeMap({ id: m.get('id'), label: m.get('label') }));
|
||||
return metadata;
|
||||
})
|
||||
.toSet()
|
||||
.filter(n => !IGNORED_COLUMNS.includes(n.get('id')))
|
||||
.toList()
|
||||
.sortBy(m => m.get('label'));
|
||||
|
||||
const relativesColumns = nodes
|
||||
.toList()
|
||||
.flatMap(n => {
|
||||
const metadata = (n.get('parents') || makeList())
|
||||
.map(m => makeMap({ id: m.get('topologyId'), label: m.get('topologyId') }));
|
||||
return metadata;
|
||||
})
|
||||
.toSet()
|
||||
.toList()
|
||||
.sortBy(m => m.get('label'));
|
||||
|
||||
return relativesColumns.concat(metadataColumns.concat(metricColumns)).toJS();
|
||||
}
|
||||
|
||||
|
||||
export default class NodesGrid extends React.Component {
|
||||
function renderIdCell(props, onClick) {
|
||||
const style = {
|
||||
width: 16,
|
||||
flex: 'none',
|
||||
color: getNodeColor(props.rank, props.label_major)
|
||||
};
|
||||
|
||||
onMouseOverRow(node) {
|
||||
enterNode(node.id);
|
||||
return (
|
||||
<div className="nodes-grid-id-column" onClick={onClick}>
|
||||
<div className="content">
|
||||
<div style={style}><i className="fa fa-square" /></div>
|
||||
<div className="truncate">
|
||||
{props.label} {props.pseudo &&
|
||||
<span className="nodes-grid-label-minor">{props.label_minor}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
class NodesGrid extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.renderIdCell = this.renderIdCell.bind(this);
|
||||
this.clickRow = this.clickRow.bind(this);
|
||||
|
||||
this.onSortChange = this.onSortChange.bind(this);
|
||||
this.onMouseEnterRow = this.onMouseEnterRow.bind(this);
|
||||
this.onMouseLeaveRow = this.onMouseLeaveRow.bind(this);
|
||||
}
|
||||
|
||||
onMouseOut() {
|
||||
leaveNode();
|
||||
clickRow(ev, nodeId, nodeLabel) {
|
||||
if (ev.target.className === 'node-details-relatives-link') {
|
||||
return;
|
||||
}
|
||||
this.props.clickNode(nodeId, nodeLabel);
|
||||
}
|
||||
|
||||
renderIdCell(props) {
|
||||
return renderIdCell(props, (ev) => this.clickRow(ev, props.id, props.label));
|
||||
}
|
||||
|
||||
onMouseEnterRow() {
|
||||
this.props.clickPauseUpdate();
|
||||
}
|
||||
|
||||
onMouseLeaveRow() {
|
||||
this.props.clickResumeUpdate();
|
||||
}
|
||||
|
||||
onSortChange(sortBy, sortedDesc) {
|
||||
this.props.sortOrderChanged(sortBy, sortedDesc);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { margins, nodes, height } = this.props;
|
||||
const { margins, nodes, height, gridSortBy, gridSortedDesc,
|
||||
searchNodeMatches = makeMap(), searchQuery } = 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`,
|
||||
};
|
||||
|
||||
const detailsData = {
|
||||
label: 'procs',
|
||||
label: this.props.topology && this.props.topology.get('fullName'),
|
||||
id: '',
|
||||
nodes: nodes.toList().toJS(),
|
||||
nodes: nodes
|
||||
.toList()
|
||||
.filter(n => !searchQuery || searchNodeMatches.has(n.get('id')))
|
||||
.toJS(),
|
||||
columns: getColumns(nodes)
|
||||
};
|
||||
|
||||
@@ -52,12 +139,23 @@ export default class NodesGrid extends React.Component {
|
||||
<div className="nodes-grid">
|
||||
<NodeDetailsTable
|
||||
style={cmpStyle}
|
||||
onMouseOut={this.onMouseOut}
|
||||
onMouseOverRow={this.onMouseOverRow}
|
||||
className={className}
|
||||
renderIdCell={this.renderIdCell}
|
||||
tbodyStyle={tbodyStyle}
|
||||
topologyId={this.props.topologyId}
|
||||
onSortChange={this.onSortChange}
|
||||
{...detailsData}
|
||||
highlightedNodeIds={this.props.highlightedNodeIds}
|
||||
sortBy={gridSortBy}
|
||||
sortedDesc={gridSortedDesc}
|
||||
selectedNodeId={this.props.selectedNodeId}
|
||||
limit={1000} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
() => ({}),
|
||||
{ clickNode, sortOrderChanged, clickPauseUpdate, clickResumeUpdate }
|
||||
)(NodesGrid);
|
||||
|
||||
@@ -12,7 +12,7 @@ import Topologies from './topologies.js';
|
||||
import TopologyOptions from './topology-options.js';
|
||||
import { getApiDetails, getTopologies } from '../utils/web-api-utils';
|
||||
import { focusSearch, pinNextMetric, hitBackspace, hitEnter, hitEsc, unpinMetric,
|
||||
selectMetric, toggleHelp } from '../actions/app-actions';
|
||||
selectMetric, toggleHelp, toggleGridMode } from '../actions/app-actions';
|
||||
import Details from './details';
|
||||
import Nodes from './nodes';
|
||||
import GridModeSelector from './grid-mode-selector';
|
||||
@@ -87,6 +87,10 @@ class App extends React.Component {
|
||||
dispatch(pinNextMetric(-1));
|
||||
} else if (char === '>') {
|
||||
dispatch(pinNextMetric(1));
|
||||
} else if (char === 'v') {
|
||||
dispatch(toggleGridMode(false));
|
||||
} else if (char === 't') {
|
||||
dispatch(toggleGridMode(true));
|
||||
} else if (char === 'q') {
|
||||
dispatch(unpinMetric());
|
||||
dispatch(selectMetric(null));
|
||||
@@ -100,8 +104,8 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { showingDetails, showingHelp, showingMetricsSelector, showingNetworkSelector,
|
||||
showingTerminal } = this.props;
|
||||
const { gridMode, showingDetails, showingHelp, showingMetricsSelector,
|
||||
showingNetworkSelector, showingTerminal } = this.props;
|
||||
const isIframe = window !== window.top;
|
||||
|
||||
return (
|
||||
@@ -126,11 +130,11 @@ class App extends React.Component {
|
||||
|
||||
<Nodes />
|
||||
|
||||
<Sidebar>
|
||||
<Sidebar classNames={gridMode ? 'sidebar-gridmode' : ''}>
|
||||
{showingMetricsSelector && !gridMode && <MetricSelector />}
|
||||
{showingNetworkSelector && !gridMode && <NetworkSelector />}
|
||||
<Status />
|
||||
<GridModeSelector />
|
||||
{showingMetricsSelector && <MetricSelector />}
|
||||
{showingNetworkSelector && <NetworkSelector />}
|
||||
<TopologyOptions />
|
||||
</Sidebar>
|
||||
|
||||
@@ -143,6 +147,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'),
|
||||
|
||||
61
client/app/scripts/components/grid-mode-selector.js
Normal file
61
client/app/scripts/components/grid-mode-selector.js
Normal file
@@ -0,0 +1,61 @@
|
||||
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, label, isSelected, onClick) {
|
||||
const className = classNames('grid-mode-selector-action', {
|
||||
'grid-mode-selector-action-selected': isSelected
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={onClick} >
|
||||
<span className={icons} style={{fontSize: 12}} />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { gridMode } = this.props;
|
||||
|
||||
return (
|
||||
<div className="grid-mode-selector">
|
||||
<div className="grid-mode-selector-wrapper">
|
||||
{this.renderItem('fa fa-share-alt', 'Visualization', !gridMode, this.disableGridMode)}
|
||||
{this.renderItem('fa fa-table', 'Table', gridMode, this.enableGridMode)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
gridMode: state.get('gridMode'),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ toggleGridMode }
|
||||
)(GridModeSelector);
|
||||
@@ -4,6 +4,8 @@ const GENERAL_SHORTCUTS = [
|
||||
{key: 'esc', label: 'Close active panel'},
|
||||
{key: '/', label: 'Activate search field'},
|
||||
{key: '?', label: 'Toggle shortcut menu'},
|
||||
{key: 't', label: 'Activate Table mode'},
|
||||
{key: 'v', label: 'Activate Visualization mode'},
|
||||
];
|
||||
|
||||
const CANVAS_METRIC_SHORTCUTS = [
|
||||
|
||||
@@ -47,7 +47,7 @@ export class NodeDetails extends React.Component {
|
||||
}
|
||||
|
||||
renderTools() {
|
||||
const showSwitchTopology = this.props.index > 0;
|
||||
const showSwitchTopology = this.props.nodeId !== this.props.selectedNodeId;
|
||||
const topologyTitle = `View ${this.props.label} in ${this.props.topologyId}`;
|
||||
|
||||
return (
|
||||
@@ -229,7 +229,8 @@ function mapStateToProps(state, ownProps) {
|
||||
const currentTopologyId = state.get('currentTopologyId');
|
||||
return {
|
||||
nodeMatches: state.getIn(['searchNodeMatches', currentTopologyId, ownProps.id]),
|
||||
nodes: state.get('nodes')
|
||||
nodes: state.get('nodes'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,14 +20,14 @@ class NodeDetailsTableNodeLink extends React.Component {
|
||||
render() {
|
||||
if (this.props.linkable) {
|
||||
return (
|
||||
<span className="node-details-table-node-link truncate" title={this.props.label}
|
||||
<span className="node-details-table-node-link" title={this.props.label}
|
||||
onClick={this.handleClick}>
|
||||
{this.props.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="node-details-table-node truncate" title={this.props.label}>
|
||||
<span className="node-details-table-node" title={this.props.label}>
|
||||
{this.props.label}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { formatMetric } from '../../utils/string-utils';
|
||||
|
||||
function NodeDetailsTableNodeMetric(props) {
|
||||
return (
|
||||
<td className="node-details-table-node-metric">
|
||||
<td className="node-details-table-node-metric" style={props.style}>
|
||||
{formatMetric(props.value, props)}
|
||||
</td>
|
||||
);
|
||||
|
||||
@@ -16,57 +16,94 @@ function getValuesForNode(node) {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
(node.parents || []).forEach(p => {
|
||||
values[p.topologyId] = {
|
||||
id: p.topologyId,
|
||||
label: p.topologyId,
|
||||
value: p.label,
|
||||
relative: p,
|
||||
valueType: 'relatives',
|
||||
};
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
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 (
|
||||
<td className="node-details-table-node-value truncate" title={field.value}
|
||||
style={style}
|
||||
key={field.id}>
|
||||
{field.value}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
return <NodeDetailsTableNodeMetric key={field.id} {...field} />;
|
||||
if (field.valueType === 'relatives') {
|
||||
return (
|
||||
<td className="node-details-table-node-value truncate" title={field.value}
|
||||
style={style}
|
||||
key={field.id}>
|
||||
{<NodeDetailsTableNodeLink linkable nodeId={field.relative.id} {...field.relative} />}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
return <NodeDetailsTableNodeMetric style={style} key={field.id} {...field} />;
|
||||
}
|
||||
// empty cell to complete the row for proper hover
|
||||
return <td className="node-details-table-node-value" key={id} />;
|
||||
return <td className="node-details-table-node-value" style={style} key={id} />;
|
||||
});
|
||||
}
|
||||
|
||||
export default class NodeDetailsTableRow extends React.Component {
|
||||
|
||||
export default class NodeDetailsTableRow extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onMouseOver = this.onMouseOver.bind(this);
|
||||
this.onMouseEnter = this.onMouseEnter.bind(this);
|
||||
this.onMouseLeave = this.onMouseLeave.bind(this);
|
||||
}
|
||||
|
||||
onMouseOver() {
|
||||
const { node, onMouseOverRow } = this.props;
|
||||
onMouseOverRow(node);
|
||||
onMouseEnter() {
|
||||
const { node, onMouseEnterRow } = this.props;
|
||||
onMouseEnterRow(node);
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
const { node, onMouseLeaveRow } = this.props;
|
||||
onMouseLeaveRow(node);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node, nodeIdKey, topologyId, columns, onMouseOverRow, selected } = this.props;
|
||||
const values = renderValues(node, columns);
|
||||
const { node, nodeIdKey, topologyId, columns, onMouseEnterRow, onMouseLeaveRow, selected,
|
||||
widths } = this.props;
|
||||
const [firstColumnWidth, ...columnWidths] = widths;
|
||||
const values = renderValues(node, columns, columnWidths);
|
||||
const nodeId = node[nodeIdKey];
|
||||
const className = classNames('node-details-table-node', { selected });
|
||||
|
||||
return (
|
||||
<tr onMouseOver={onMouseOverRow && this.onMouseOver} className={className}>
|
||||
<td className="node-details-table-node-label truncate">
|
||||
<NodeDetailsTableNodeLink
|
||||
topologyId={topologyId}
|
||||
nodeId={nodeId}
|
||||
{...node} />
|
||||
<tr
|
||||
onMouseEnter={onMouseEnterRow && this.onMouseEnter}
|
||||
onMouseLeave={onMouseLeaveRow && this.onMouseLeave}
|
||||
className={className}>
|
||||
<td className="node-details-table-node-label truncate"
|
||||
style={{ width: firstColumnWidth }}>
|
||||
{this.props.renderIdCell(Object.assign(node, {topologyId, nodeId}))}
|
||||
</td>
|
||||
{values}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NodeDetailsTableRow.defaultProps = {
|
||||
renderIdCell: (props) => <NodeDetailsTableNodeLink {...props} />
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ShowMore from '../show-more';
|
||||
import NodeDetailsTableRow from './node-details-table-row';
|
||||
@@ -13,9 +14,23 @@ function isNumberField(field) {
|
||||
|
||||
const COLUMN_WIDTHS = {
|
||||
port: '44px',
|
||||
count: '70px'
|
||||
count: '70px',
|
||||
process_cpu_usage_percent: '80px',
|
||||
threads: '80px',
|
||||
process_memory_usage_bytes: '80px',
|
||||
docker_cpu_total_usage: '80px',
|
||||
docker_memory_usage: '80px',
|
||||
docker_container_uptime: '85px',
|
||||
docker_container_restart_count: '80px',
|
||||
docker_container_ips: '80px',
|
||||
docker_container_created: '110px',
|
||||
docker_container_state_human: '120px',
|
||||
open_files_count: '80px',
|
||||
ppid: '80px',
|
||||
pid: '80px',
|
||||
};
|
||||
|
||||
|
||||
function getDefaultSortBy(columns, nodes) {
|
||||
// default sorter specified by columns
|
||||
const defaultSortColumn = _.find(columns, {defaultSort: true});
|
||||
@@ -31,7 +46,15 @@ function getValueForSortBy(sortBy) {
|
||||
// return the node's value based on the sortBy field
|
||||
return (node) => {
|
||||
if (sortBy !== null) {
|
||||
const field = _.union(node.metrics, node.metadata).find(f => f.id === sortBy);
|
||||
let field = _.union(node.metrics, node.metadata).find(f => f.id === sortBy);
|
||||
|
||||
if (!field && node.parents) {
|
||||
field = node.parents.find(f => f.topologyId === sortBy);
|
||||
if (field) {
|
||||
return field.label;
|
||||
}
|
||||
}
|
||||
|
||||
if (field) {
|
||||
if (isNumberField(field)) {
|
||||
return parseFloat(field.value);
|
||||
@@ -39,7 +62,8 @@ function getValueForSortBy(sortBy) {
|
||||
return field.value;
|
||||
}
|
||||
}
|
||||
return -1e-10; // just under 0 to treat missing values differently from 0
|
||||
|
||||
return '';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,6 +99,33 @@ function getSortedNodes(nodes, columns, sortBy, sortedDesc) {
|
||||
}
|
||||
|
||||
|
||||
function getColumnsWidths(headers) {
|
||||
return headers.map((h, i) => {
|
||||
//
|
||||
// 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%';
|
||||
} else if (headers.length > 5) {
|
||||
return '20%';
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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) {
|
||||
@@ -82,8 +133,8 @@ export default class NodeDetailsTable extends React.Component {
|
||||
this.DEFAULT_LIMIT = 5;
|
||||
this.state = {
|
||||
limit: props.limit || this.DEFAULT_LIMIT,
|
||||
sortedDesc: true,
|
||||
sortBy: null
|
||||
sortedDesc: this.props.sortedDesc,
|
||||
sortBy: this.props.sortBy
|
||||
};
|
||||
this.handleLimitClick = this.handleLimitClick.bind(this);
|
||||
}
|
||||
@@ -94,6 +145,7 @@ export default class NodeDetailsTable extends React.Component {
|
||||
? !this.state.sortedDesc : this.state.sortedDesc;
|
||||
const sortBy = headerId;
|
||||
this.setState({sortBy, sortedDesc});
|
||||
this.props.onSortChange(sortBy, sortedDesc);
|
||||
}
|
||||
|
||||
handleLimitClick() {
|
||||
@@ -101,56 +153,42 @@ 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 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];
|
||||
});
|
||||
const headers = this.getColumnHeaders();
|
||||
const widths = getColumnsWidths(headers);
|
||||
const defaultSortBy = getDefaultSortBy(this.props.columns, this.props.nodes);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
{headers.map(header => {
|
||||
{headers.map((header, i) => {
|
||||
const headerClasses = ['node-details-table-header', 'truncate'];
|
||||
const onHeaderClick = ev => {
|
||||
this.handleHeaderClick(ev, header.id);
|
||||
};
|
||||
|
||||
// sort by first metric by default
|
||||
const isSorted = this.state.sortBy !== null
|
||||
? header.id === this.state.sortBy : header.id === defaultSortBy;
|
||||
const isSorted = header.id === (this.state.sortBy || defaultSortBy);
|
||||
const isSortedDesc = isSorted && this.state.sortedDesc;
|
||||
const isSortedAsc = isSorted && !isSortedDesc;
|
||||
|
||||
if (isSorted) {
|
||||
headerClasses.push('node-details-table-header-sorted');
|
||||
}
|
||||
|
||||
// set header width in percent
|
||||
const style = {};
|
||||
if (header.width) {
|
||||
style.width = header.width;
|
||||
if (widths[i]) {
|
||||
style.width = widths[i];
|
||||
}
|
||||
|
||||
return (
|
||||
<td className={headerClasses.join(' ')} style={style} onClick={onHeaderClick}
|
||||
key={header.id}>
|
||||
title={header.label} key={header.id}>
|
||||
{isSortedAsc
|
||||
&& <span className="node-details-table-header-sorter fa fa-caret-up" />}
|
||||
{isSortedDesc
|
||||
@@ -167,7 +205,8 @@ export default class NodeDetailsTable extends React.Component {
|
||||
|
||||
render() {
|
||||
const headers = this.renderHeaders();
|
||||
const { nodeIdKey, columns, topologyId, onMouseOverRow } = this.props;
|
||||
const { nodeIdKey, columns, topologyId, onMouseEnter, onMouseLeave, onMouseEnterRow,
|
||||
onMouseLeaveRow } = this.props;
|
||||
let nodes = getSortedNodes(this.props.nodes, this.props.columns, this.state.sortBy,
|
||||
this.state.sortedDesc);
|
||||
const limited = nodes && this.state.limit > 0 && nodes.length > this.state.limit;
|
||||
@@ -183,23 +222,29 @@ export default class NodeDetailsTable extends React.Component {
|
||||
React.cloneElement(child, { nodeOrder })
|
||||
));
|
||||
|
||||
const className = classNames('node-details-table-wrapper-wrapper', this.props.className);
|
||||
|
||||
return (
|
||||
<div className="node-details-table-wrapper-wrapper" style={this.props.style}>
|
||||
<div className="node-details-table-wrapper" onMouseOut={this.props.onMouseOut}>
|
||||
<div className={className}
|
||||
style={this.props.style}>
|
||||
<div className="node-details-table-wrapper">
|
||||
<table className="node-details-table">
|
||||
<thead>
|
||||
{headers}
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody style={this.props.tbodyStyle} onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}>
|
||||
{nodes && nodes.map(node => (
|
||||
<NodeDetailsTableRow
|
||||
key={node.id}
|
||||
selected={this.props.highlightedNodeIds &&
|
||||
this.props.highlightedNodeIds.has(node.id)}
|
||||
renderIdCell={this.props.renderIdCell}
|
||||
selected={this.props.selectedNodeId === node.id}
|
||||
node={node}
|
||||
nodeIdKey={nodeIdKey}
|
||||
widths={getColumnsWidths(this.getColumnHeaders())}
|
||||
columns={columns}
|
||||
onMouseOverRow={onMouseOverRow}
|
||||
onMouseLeaveRow={onMouseLeaveRow}
|
||||
onMouseEnterRow={onMouseEnterRow}
|
||||
topologyId={topologyId} />
|
||||
))}
|
||||
</tbody>
|
||||
@@ -218,5 +263,7 @@ export default class NodeDetailsTable extends React.Component {
|
||||
|
||||
|
||||
NodeDetailsTable.defaultProps = {
|
||||
nodeIdKey: 'id' // key to identify a node in a row (used for topology links)
|
||||
nodeIdKey: 'id', // key to identify a node in a row (used for topology links)
|
||||
onSortChange: () => {},
|
||||
sortedDesc: true,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Loading, getNodeType } from './loading';
|
||||
import { isTopologyEmpty } from '../utils/topology-utils';
|
||||
import { CANVAS_MARGINS } from '../constants/styles';
|
||||
|
||||
const navbarHeight = 160;
|
||||
const navbarHeight = 194;
|
||||
const marginTop = 0;
|
||||
|
||||
|
||||
@@ -67,8 +67,9 @@ class Nodes extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nodes, topologyEmpty, topologiesLoaded, nodesLoaded, topologies,
|
||||
topology, highlightedNodeIds } = this.props;
|
||||
const { nodes, topologyEmpty, selectedNodeId, gridMode, gridSortBy,
|
||||
topologiesLoaded, nodesLoaded, topologies, topology,
|
||||
gridSortedDesc, searchNodeMatches, searchQuery } = this.props;
|
||||
const layoutPrecision = getLayoutPrecision(nodes.size);
|
||||
|
||||
return (
|
||||
@@ -80,15 +81,22 @@ class Nodes extends React.Component {
|
||||
show={topologiesLoaded && !nodesLoaded} />
|
||||
</DelayedShow>
|
||||
{this.renderEmptyTopologyError(topologiesLoaded && nodesLoaded && topologyEmpty)}
|
||||
{this.props.gridMode ?
|
||||
{gridMode ?
|
||||
<NodesGrid {...this.state}
|
||||
nodeSize="24"
|
||||
nodes={nodes}
|
||||
topology={this.props.currentTopology}
|
||||
topologyId={this.props.currentTopologyId}
|
||||
margins={CANVAS_MARGINS}
|
||||
layoutPrecision={layoutPrecision}
|
||||
highlightedNodeIds={highlightedNodeIds}
|
||||
selectedNodeId={selectedNodeId}
|
||||
gridSortBy={gridSortBy}
|
||||
gridSortedDesc={gridSortedDesc}
|
||||
searchNodeMatches={searchNodeMatches}
|
||||
searchQuery={searchQuery}
|
||||
/> :
|
||||
<NodesChart {...this.state}
|
||||
nodes={nodes}
|
||||
margins={CANVAS_MARGINS}
|
||||
layoutPrecision={layoutPrecision}
|
||||
/>}
|
||||
@@ -111,13 +119,20 @@ class Nodes extends React.Component {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
gridMode: state.get('gridMode'),
|
||||
nodes: state.get('nodes'),
|
||||
nodesLoaded: state.get('nodesLoaded'),
|
||||
topologies: state.get('topologies'),
|
||||
topologiesLoaded: state.get('topologiesLoaded'),
|
||||
gridSortBy: state.get('gridSortBy'),
|
||||
gridSortedDesc: state.get('gridSortedDesc'),
|
||||
nodes: state.get('nodes').filter(node => !node.get('filtered')),
|
||||
currentTopology: state.get('currentTopology'),
|
||||
currentTopologyId: state.get('currentTopologyId'),
|
||||
topologyEmpty: isTopologyEmpty(state),
|
||||
topology: state.get('currentTopology'),
|
||||
highlightedNodeIds: state.get('highlightedNodeIds')
|
||||
searchNodeMatches: state.getIn(['searchNodeMatches', state.get('currentTopologyId')]),
|
||||
searchQuery: state.get('searchQuery'),
|
||||
selectedNodeId: state.get('selectedNodeId')
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Sidebar({children}) {
|
||||
export default function Sidebar({children, classNames}) {
|
||||
const className = `sidebar ${classNames}`;
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ export const DETAILS_PANEL_OFFSET = 8;
|
||||
export const CANVAS_METRIC_FONT_SIZE = 0.19;
|
||||
|
||||
export const CANVAS_MARGINS = {
|
||||
top: 130,
|
||||
top: 160,
|
||||
left: 40,
|
||||
right: 40,
|
||||
bottom: 100,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { applyPinnedSearches, updateNodeMatches } from '../utils/search-utils';
|
||||
import { getNetworkNodes, getAvailableNetworks } from '../utils/network-view-utils';
|
||||
import { findTopologyById, getAdjacentNodes, setTopologyUrlsById,
|
||||
updateTopologyIds, filterHiddenTopologies } from '../utils/topology-utils';
|
||||
updateTopologyIds, filterHiddenTopologies, addTopologyFullname } from '../utils/topology-utils';
|
||||
|
||||
const log = debug('scope:app-store');
|
||||
const error = debug('scope:error');
|
||||
@@ -29,6 +29,8 @@ export const initialState = makeMap({
|
||||
errorUrl: null,
|
||||
forceRelayout: false,
|
||||
gridMode: false,
|
||||
gridSortBy: null,
|
||||
gridSortedDesc: true,
|
||||
highlightedEdgeIds: makeSet(),
|
||||
highlightedNodeIds: makeSet(),
|
||||
hostname: '...',
|
||||
@@ -80,7 +82,8 @@ function processTopologies(state, nextTopologies) {
|
||||
state = state.set('topologyUrlsById',
|
||||
setTopologyUrlsById(state.get('topologyUrlsById'), topologiesWithId));
|
||||
|
||||
const immNextTopologies = fromJS(topologiesWithId).sortBy(topologySorter);
|
||||
const topologiesWithFullnames = addTopologyFullname(topologiesWithId);
|
||||
const immNextTopologies = fromJS(topologiesWithFullnames).sortBy(topologySorter);
|
||||
return state.mergeDeepIn(['topologies'], immNextTopologies);
|
||||
}
|
||||
|
||||
@@ -167,6 +170,13 @@ export function rootReducer(state = initialState, action) {
|
||||
return state.set('exportingGraph', action.exporting);
|
||||
}
|
||||
|
||||
case ActionTypes.SORT_ORDER_CHANGED: {
|
||||
return state.merge({
|
||||
gridSortBy: action.sortBy,
|
||||
gridSortedDesc: action.sortedDesc,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.SET_GRID_MODE: {
|
||||
return state.setIn(['gridMode'], action.enabled);
|
||||
}
|
||||
@@ -631,6 +641,12 @@ export function rootReducer(state = initialState, action) {
|
||||
pinnedMetricType: action.state.pinnedMetricType
|
||||
});
|
||||
state = state.set('gridMode', action.state.mode === 'grid');
|
||||
if (action.state.gridSortBy) {
|
||||
state = state.set('gridSortBy', action.state.gridSortBy);
|
||||
}
|
||||
if (action.state.gridSortedDesc !== undefined) {
|
||||
state = state.set('gridSortedDesc', action.state.gridSortedDesc);
|
||||
}
|
||||
if (action.state.showingNetworks) {
|
||||
state = state.set('showingNetworks', action.state.showingNetworks);
|
||||
}
|
||||
|
||||
@@ -1,749 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import debug from 'debug';
|
||||
import { fromJS, is as isDeepEqual, List, Map, OrderedMap, Set } from 'immutable';
|
||||
import { Store } from 'flux/utils';
|
||||
|
||||
import AppDispatcher from '../dispatcher/app-dispatcher';
|
||||
import ActionTypes from '../constants/action-types';
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { findTopologyById, setTopologyUrlsById, updateTopologyIds,
|
||||
filterHiddenTopologies } from '../utils/topology-utils';
|
||||
|
||||
const makeList = List;
|
||||
const makeMap = Map;
|
||||
const makeOrderedMap = OrderedMap;
|
||||
const makeSet = Set;
|
||||
const log = debug('scope:app-store');
|
||||
|
||||
const error = debug('scope:error');
|
||||
|
||||
// Helpers
|
||||
|
||||
function makeNode(node) {
|
||||
return {
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
label_minor: node.label_minor,
|
||||
node_count: node.node_count,
|
||||
rank: node.rank,
|
||||
pseudo: node.pseudo,
|
||||
stack: node.stack,
|
||||
shape: node.shape,
|
||||
adjacency: node.adjacency,
|
||||
metrics: node.metrics
|
||||
};
|
||||
}
|
||||
|
||||
// Initial values
|
||||
|
||||
let topologyOptions = makeOrderedMap(); // topologyId -> options
|
||||
let controlStatus = makeMap();
|
||||
let currentTopology = null;
|
||||
let currentTopologyId = 'containers';
|
||||
let errorUrl = null;
|
||||
let forceRelayout = false;
|
||||
let highlightedEdgeIds = makeSet();
|
||||
let highlightedNodeIds = makeSet();
|
||||
let hostname = '...';
|
||||
let version = '...';
|
||||
let versionUpdate = null;
|
||||
let plugins = [];
|
||||
let mouseOverEdgeId = null;
|
||||
let mouseOverNodeId = null;
|
||||
let nodeDetails = makeOrderedMap(); // nodeId -> details
|
||||
let nodes = makeOrderedMap(); // nodeId -> node
|
||||
let selectedNodeId = null;
|
||||
let topologies = makeList();
|
||||
let topologiesLoaded = false;
|
||||
let topologyUrlsById = makeOrderedMap(); // topologyId -> topologyUrl
|
||||
let routeSet = false;
|
||||
let controlPipes = makeOrderedMap(); // pipeId -> controlPipe
|
||||
let updatePausedAt = null; // Date
|
||||
let websocketClosed = true;
|
||||
let showingHelp = false;
|
||||
let tableSortOrder = null;
|
||||
|
||||
let selectedMetric = null;
|
||||
let pinnedMetric = selectedMetric;
|
||||
// class of metric, e.g. 'cpu', rather than 'host_cpu' or 'process_cpu'.
|
||||
// allows us to keep the same metric "type" selected when the topology changes.
|
||||
let pinnedMetricType = null;
|
||||
let availableCanvasMetrics = makeList();
|
||||
|
||||
|
||||
const topologySorter = topology => topology.get('rank');
|
||||
|
||||
// adds ID field to topology (based on last part of URL path) and save urls in
|
||||
// map for easy lookup
|
||||
function processTopologies(nextTopologies) {
|
||||
// filter out hidden topos
|
||||
const visibleTopologies = filterHiddenTopologies(nextTopologies);
|
||||
|
||||
// add IDs to topology objects in-place
|
||||
const topologiesWithId = updateTopologyIds(visibleTopologies);
|
||||
|
||||
// cache URLs by ID
|
||||
topologyUrlsById = setTopologyUrlsById(topologyUrlsById, topologiesWithId);
|
||||
|
||||
const immNextTopologies = fromJS(topologiesWithId).sortBy(topologySorter);
|
||||
topologies = topologies.mergeDeep(immNextTopologies);
|
||||
}
|
||||
|
||||
function setTopology(topologyId) {
|
||||
currentTopology = findTopologyById(topologies, topologyId);
|
||||
currentTopologyId = topologyId;
|
||||
}
|
||||
|
||||
function setDefaultTopologyOptions(topologyList) {
|
||||
topologyList.forEach(topology => {
|
||||
let defaultOptions = makeOrderedMap();
|
||||
if (topology.has('options') && topology.get('options')) {
|
||||
topology.get('options').forEach((option) => {
|
||||
const optionId = option.get('id');
|
||||
const defaultValue = option.get('defaultValue');
|
||||
defaultOptions = defaultOptions.set(optionId, defaultValue);
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultOptions.size) {
|
||||
topologyOptions = topologyOptions.set(
|
||||
topology.get('id'),
|
||||
defaultOptions
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeNodeDetails(nodeId) {
|
||||
if (nodeDetails.size > 0) {
|
||||
const popNodeId = nodeId || nodeDetails.keySeq().last();
|
||||
// remove pipe if it belongs to the node being closed
|
||||
controlPipes = controlPipes.filter(pipe => pipe.get('nodeId') !== popNodeId);
|
||||
nodeDetails = nodeDetails.delete(popNodeId);
|
||||
}
|
||||
if (nodeDetails.size === 0 || selectedNodeId === nodeId) {
|
||||
selectedNodeId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllNodeDetails() {
|
||||
while (nodeDetails.size) {
|
||||
closeNodeDetails();
|
||||
}
|
||||
}
|
||||
|
||||
function resumeUpdate() {
|
||||
updatePausedAt = null;
|
||||
}
|
||||
|
||||
// Store API
|
||||
|
||||
export class AppStore extends Store {
|
||||
|
||||
// keep at the top
|
||||
getAppState() {
|
||||
const cp = this.getControlPipe();
|
||||
return {
|
||||
controlPipe: cp ? cp.toJS() : null,
|
||||
nodeDetails: this.getNodeDetailsState().toJS(),
|
||||
selectedNodeId,
|
||||
pinnedMetricType,
|
||||
topologyId: currentTopologyId,
|
||||
topologyOptions: topologyOptions.toJS() // all options
|
||||
};
|
||||
}
|
||||
|
||||
getTableSortOrder() {
|
||||
return tableSortOrder;
|
||||
}
|
||||
|
||||
getShowingHelp() {
|
||||
return showingHelp;
|
||||
}
|
||||
|
||||
getActiveTopologyOptions() {
|
||||
// options for current topology, sub-topologies share options with parent
|
||||
if (currentTopology && currentTopology.get('parentId')) {
|
||||
return topologyOptions.get(currentTopology.get('parentId'));
|
||||
}
|
||||
return topologyOptions.get(currentTopologyId);
|
||||
}
|
||||
|
||||
getAdjacentNodes(nodeId) {
|
||||
let adjacentNodes = makeSet();
|
||||
|
||||
if (nodes.has(nodeId)) {
|
||||
adjacentNodes = makeSet(nodes.getIn([nodeId, 'adjacency']));
|
||||
// fill up set with reverse edges
|
||||
nodes.forEach((node, id) => {
|
||||
if (node.get('adjacency') && node.get('adjacency').includes(nodeId)) {
|
||||
adjacentNodes = adjacentNodes.add(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return adjacentNodes;
|
||||
}
|
||||
|
||||
getPinnedMetric() {
|
||||
return pinnedMetric;
|
||||
}
|
||||
|
||||
getSelectedMetric() {
|
||||
return selectedMetric;
|
||||
}
|
||||
|
||||
getAvailableCanvasMetrics() {
|
||||
return availableCanvasMetrics;
|
||||
}
|
||||
|
||||
getAvailableCanvasMetricsTypes() {
|
||||
return makeMap(this.getAvailableCanvasMetrics().map(m => [m.get('id'), m.get('label')]));
|
||||
}
|
||||
|
||||
getControlStatus() {
|
||||
return controlStatus;
|
||||
}
|
||||
|
||||
getControlPipe() {
|
||||
return controlPipes.last();
|
||||
}
|
||||
|
||||
getCurrentTopology() {
|
||||
if (!currentTopology) {
|
||||
currentTopology = setTopology(currentTopologyId);
|
||||
}
|
||||
return currentTopology;
|
||||
}
|
||||
|
||||
getCurrentTopologyId() {
|
||||
return currentTopologyId;
|
||||
}
|
||||
|
||||
getCurrentTopologyOptions() {
|
||||
return currentTopology && currentTopology.get('options') || makeOrderedMap();
|
||||
}
|
||||
|
||||
getCurrentTopologyUrl() {
|
||||
return currentTopology && currentTopology.get('url');
|
||||
}
|
||||
|
||||
getErrorUrl() {
|
||||
return errorUrl;
|
||||
}
|
||||
|
||||
getHighlightedEdgeIds() {
|
||||
return highlightedEdgeIds;
|
||||
}
|
||||
|
||||
getHighlightedNodeIds() {
|
||||
return highlightedNodeIds;
|
||||
}
|
||||
|
||||
getHostname() {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
getNodeDetails() {
|
||||
return nodeDetails;
|
||||
}
|
||||
|
||||
getNodeDetailsState() {
|
||||
return nodeDetails.toIndexedSeq().map(details => ({
|
||||
id: details.id, label: details.label, topologyId: details.topologyId
|
||||
}));
|
||||
}
|
||||
|
||||
getTopCardNodeId() {
|
||||
return nodeDetails.last() && nodeDetails.last().id;
|
||||
}
|
||||
|
||||
getNodes() {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
getSelectedNodeId() {
|
||||
return selectedNodeId;
|
||||
}
|
||||
|
||||
getTopologies() {
|
||||
return topologies;
|
||||
}
|
||||
|
||||
getTopologyUrlsById() {
|
||||
return topologyUrlsById;
|
||||
}
|
||||
|
||||
getUpdatePausedAt() {
|
||||
return updatePausedAt;
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
getVersionUpdate() {
|
||||
return versionUpdate;
|
||||
}
|
||||
|
||||
getPlugins() {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
isForceRelayout() {
|
||||
return forceRelayout;
|
||||
}
|
||||
|
||||
isRouteSet() {
|
||||
return routeSet;
|
||||
}
|
||||
|
||||
isTopologiesLoaded() {
|
||||
return topologiesLoaded;
|
||||
}
|
||||
|
||||
isTopologyEmpty() {
|
||||
return currentTopology && currentTopology.get('stats')
|
||||
&& currentTopology.get('stats').get('node_count') === 0 && nodes.size === 0;
|
||||
}
|
||||
|
||||
isUpdatePaused() {
|
||||
return updatePausedAt !== null;
|
||||
}
|
||||
|
||||
isWebsocketClosed() {
|
||||
return websocketClosed;
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
if (!payload.type) {
|
||||
error('Payload missing a type!', payload);
|
||||
}
|
||||
|
||||
switch (payload.type) {
|
||||
case ActionTypes.CHANGE_TOPOLOGY_OPTION: {
|
||||
resumeUpdate();
|
||||
// set option on parent topology
|
||||
const topology = findTopologyById(topologies, payload.topologyId);
|
||||
if (topology) {
|
||||
const topologyId = topology.get('parentId') || topology.get('id');
|
||||
if (topologyOptions.getIn([topologyId, payload.option]) !== payload.value) {
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
topologyOptions = topologyOptions.setIn(
|
||||
[topologyId, payload.option],
|
||||
payload.value
|
||||
);
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLEAR_CONTROL_ERROR: {
|
||||
controlStatus = controlStatus.removeIn([payload.nodeId, 'error']);
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_BACKGROUND: {
|
||||
closeAllNodeDetails();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_CLOSE_DETAILS: {
|
||||
closeNodeDetails(payload.nodeId);
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.SORT_ORDER_CHANGED: {
|
||||
tableSortOrder = makeMap((payload.newOrder || []).map((n, i) => [n.id, i]));
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_CLOSE_TERMINAL: {
|
||||
controlPipes = controlPipes.clear();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_FORCE_RELAYOUT: {
|
||||
forceRelayout = true;
|
||||
// fire only once, reset after emitChange
|
||||
setTimeout(() => {
|
||||
forceRelayout = false;
|
||||
}, 0);
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_NODE: {
|
||||
const prevSelectedNodeId = selectedNodeId;
|
||||
const prevDetailsStackSize = nodeDetails.size;
|
||||
// click on sibling closes all
|
||||
closeAllNodeDetails();
|
||||
// select new node if it's not the same (in that case just delesect)
|
||||
if (prevDetailsStackSize > 1 || prevSelectedNodeId !== payload.nodeId) {
|
||||
// dont set origin if a node was already selected, suppresses animation
|
||||
const origin = prevSelectedNodeId === null ? payload.origin : null;
|
||||
nodeDetails = nodeDetails.set(
|
||||
payload.nodeId,
|
||||
{
|
||||
id: payload.nodeId,
|
||||
label: payload.label,
|
||||
origin,
|
||||
topologyId: currentTopologyId
|
||||
}
|
||||
);
|
||||
selectedNodeId = payload.nodeId;
|
||||
}
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_PAUSE_UPDATE: {
|
||||
updatePausedAt = new Date;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_RELATIVE: {
|
||||
if (nodeDetails.has(payload.nodeId)) {
|
||||
// bring to front
|
||||
const details = nodeDetails.get(payload.nodeId);
|
||||
nodeDetails = nodeDetails.delete(payload.nodeId);
|
||||
nodeDetails = nodeDetails.set(payload.nodeId, details);
|
||||
} else {
|
||||
nodeDetails = nodeDetails.set(
|
||||
payload.nodeId,
|
||||
{
|
||||
id: payload.nodeId,
|
||||
label: payload.label,
|
||||
origin: payload.origin,
|
||||
topologyId: payload.topologyId
|
||||
}
|
||||
);
|
||||
}
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_RESUME_UPDATE: {
|
||||
resumeUpdate();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE: {
|
||||
resumeUpdate();
|
||||
nodeDetails = nodeDetails.filter((v, k) => k === payload.nodeId);
|
||||
controlPipes = controlPipes.clear();
|
||||
selectedNodeId = payload.nodeId;
|
||||
if (payload.topologyId !== currentTopologyId) {
|
||||
setTopology(payload.topologyId);
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
availableCanvasMetrics = makeList();
|
||||
tableSortOrder = null;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLICK_TOPOLOGY: {
|
||||
resumeUpdate();
|
||||
closeAllNodeDetails();
|
||||
if (payload.topologyId !== currentTopologyId) {
|
||||
setTopology(payload.topologyId);
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
availableCanvasMetrics = makeList();
|
||||
tableSortOrder = null;
|
||||
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CLOSE_WEBSOCKET: {
|
||||
if (!websocketClosed) {
|
||||
websocketClosed = true;
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.SELECT_METRIC: {
|
||||
selectedMetric = payload.metricId;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.PIN_METRIC: {
|
||||
pinnedMetric = payload.metricId;
|
||||
pinnedMetricType = this.getAvailableCanvasMetricsTypes().get(payload.metricId);
|
||||
selectedMetric = payload.metricId;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.UNPIN_METRIC: {
|
||||
pinnedMetric = null;
|
||||
pinnedMetricType = null;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.SHOW_HELP: {
|
||||
showingHelp = true;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.HIDE_HELP: {
|
||||
showingHelp = false;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.DESELECT_NODE: {
|
||||
closeNodeDetails();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.DO_CONTROL: {
|
||||
controlStatus = controlStatus.set(payload.nodeId, makeMap({
|
||||
pending: true,
|
||||
error: null
|
||||
}));
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.ENTER_EDGE: {
|
||||
// clear old highlights
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
|
||||
// highlight edge
|
||||
highlightedEdgeIds = highlightedEdgeIds.add(payload.edgeId);
|
||||
|
||||
// highlight adjacent nodes
|
||||
highlightedNodeIds = highlightedNodeIds.union(payload.edgeId.split(EDGE_ID_SEPARATOR));
|
||||
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.ENTER_NODE: {
|
||||
const nodeId = payload.nodeId;
|
||||
const adjacentNodes = this.getAdjacentNodes(nodeId);
|
||||
|
||||
// clear old highlights
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
|
||||
// highlight nodes
|
||||
highlightedNodeIds = highlightedNodeIds.add(nodeId);
|
||||
highlightedNodeIds = highlightedNodeIds.union(adjacentNodes);
|
||||
|
||||
// highlight edges
|
||||
if (adjacentNodes.size > 0) {
|
||||
// all neighbour combinations because we dont know which direction exists
|
||||
highlightedEdgeIds = highlightedEdgeIds.union(adjacentNodes.flatMap((adjacentId) => [
|
||||
[adjacentId, nodeId].join(EDGE_ID_SEPARATOR),
|
||||
[nodeId, adjacentId].join(EDGE_ID_SEPARATOR)
|
||||
]));
|
||||
}
|
||||
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.LEAVE_EDGE: {
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.LEAVE_NODE: {
|
||||
highlightedEdgeIds = highlightedEdgeIds.clear();
|
||||
highlightedNodeIds = highlightedNodeIds.clear();
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.OPEN_WEBSOCKET: {
|
||||
// flush nodes cache after re-connect
|
||||
nodes = nodes.clear();
|
||||
websocketClosed = false;
|
||||
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.DO_CONTROL_ERROR: {
|
||||
controlStatus = controlStatus.set(payload.nodeId, makeMap({
|
||||
pending: false,
|
||||
error: payload.error
|
||||
}));
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.DO_CONTROL_SUCCESS: {
|
||||
controlStatus = controlStatus.set(payload.nodeId, makeMap({
|
||||
pending: false,
|
||||
error: null
|
||||
}));
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_CONTROL_PIPE: {
|
||||
controlPipes = controlPipes.set(payload.pipeId, makeOrderedMap({
|
||||
id: payload.pipeId,
|
||||
nodeId: payload.nodeId,
|
||||
raw: payload.rawTty
|
||||
}));
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS: {
|
||||
if (controlPipes.has(payload.pipeId)) {
|
||||
controlPipes = controlPipes.setIn([payload.pipeId, 'status'], payload.status);
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_ERROR: {
|
||||
if (errorUrl !== null) {
|
||||
errorUrl = payload.errorUrl;
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_NODE_DETAILS: {
|
||||
errorUrl = null;
|
||||
|
||||
// disregard if node is not selected anymore
|
||||
if (nodeDetails.has(payload.details.id)) {
|
||||
nodeDetails = nodeDetails.update(payload.details.id, obj => {
|
||||
const result = Object.assign({}, obj);
|
||||
result.notFound = false;
|
||||
result.details = payload.details;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_NODES_DELTA: {
|
||||
const emptyMessage = !payload.delta.add && !payload.delta.remove
|
||||
&& !payload.delta.update;
|
||||
// this action is called frequently, good to check if something changed
|
||||
const emitChange = !emptyMessage || errorUrl !== null;
|
||||
|
||||
if (!emptyMessage) {
|
||||
log('RECEIVE_NODES_DELTA',
|
||||
'remove', _.size(payload.delta.remove),
|
||||
'update', _.size(payload.delta.update),
|
||||
'add', _.size(payload.delta.add));
|
||||
}
|
||||
|
||||
errorUrl = null;
|
||||
|
||||
// nodes that no longer exist
|
||||
_.each(payload.delta.remove, (nodeId) => {
|
||||
// in case node disappears before mouseleave event
|
||||
if (mouseOverNodeId === nodeId) {
|
||||
mouseOverNodeId = null;
|
||||
}
|
||||
if (nodes.has(nodeId) && _.includes(mouseOverEdgeId, nodeId)) {
|
||||
mouseOverEdgeId = null;
|
||||
}
|
||||
nodes = nodes.delete(nodeId);
|
||||
});
|
||||
|
||||
// update existing nodes
|
||||
_.each(payload.delta.update, (node) => {
|
||||
if (nodes.has(node.id)) {
|
||||
nodes = nodes.set(node.id, nodes.get(node.id).merge(fromJS(node)));
|
||||
}
|
||||
});
|
||||
|
||||
// add new nodes
|
||||
_.each(payload.delta.add, (node) => {
|
||||
nodes = nodes.set(node.id, fromJS(makeNode(node)));
|
||||
});
|
||||
|
||||
availableCanvasMetrics = nodes
|
||||
.valueSeq()
|
||||
.flatMap(n => (n.get('metrics') || makeList()).map(m => (
|
||||
makeMap({id: m.get('id'), label: m.get('label')})
|
||||
)))
|
||||
.toSet()
|
||||
.toList()
|
||||
.sortBy(m => m.get('label'));
|
||||
|
||||
const similarTypeMetric = availableCanvasMetrics
|
||||
.find(m => m.get('label') === pinnedMetricType);
|
||||
pinnedMetric = similarTypeMetric && similarTypeMetric.get('id');
|
||||
// if something in the current topo is not already selected, select it.
|
||||
if (!availableCanvasMetrics.map(m => m.get('id')).toSet().has(selectedMetric)) {
|
||||
selectedMetric = pinnedMetric;
|
||||
}
|
||||
|
||||
if (emitChange) {
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_NOT_FOUND: {
|
||||
if (nodeDetails.has(payload.nodeId)) {
|
||||
nodeDetails = nodeDetails.update(payload.nodeId, obj => {
|
||||
const result = Object.assign({}, obj);
|
||||
result.notFound = true;
|
||||
return result;
|
||||
});
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_TOPOLOGIES: {
|
||||
errorUrl = null;
|
||||
topologyUrlsById = topologyUrlsById.clear();
|
||||
processTopologies(payload.topologies);
|
||||
setTopology(currentTopologyId);
|
||||
// only set on first load, if options are not already set via route
|
||||
if (!topologiesLoaded && topologyOptions.size === 0) {
|
||||
setDefaultTopologyOptions(topologies);
|
||||
}
|
||||
topologiesLoaded = true;
|
||||
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.RECEIVE_API_DETAILS: {
|
||||
errorUrl = null;
|
||||
hostname = payload.hostname;
|
||||
version = payload.version;
|
||||
plugins = payload.plugins;
|
||||
versionUpdate = payload.newVersion;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
case ActionTypes.ROUTE_TOPOLOGY: {
|
||||
routeSet = true;
|
||||
if (currentTopologyId !== payload.state.topologyId) {
|
||||
nodes = nodes.clear();
|
||||
}
|
||||
setTopology(payload.state.topologyId);
|
||||
setDefaultTopologyOptions(topologies);
|
||||
selectedNodeId = payload.state.selectedNodeId;
|
||||
pinnedMetricType = payload.state.pinnedMetricType;
|
||||
if (payload.state.controlPipe) {
|
||||
controlPipes = makeOrderedMap({
|
||||
[payload.state.controlPipe.id]:
|
||||
makeOrderedMap(payload.state.controlPipe)
|
||||
});
|
||||
} else {
|
||||
controlPipes = controlPipes.clear();
|
||||
}
|
||||
if (payload.state.nodeDetails) {
|
||||
const payloadNodeDetails = makeOrderedMap(
|
||||
payload.state.nodeDetails.map(obj => [obj.id, obj]));
|
||||
// check if detail IDs have changed
|
||||
if (!isDeepEqual(nodeDetails.keySeq(), payloadNodeDetails.keySeq())) {
|
||||
nodeDetails = payloadNodeDetails;
|
||||
}
|
||||
} else {
|
||||
nodeDetails = nodeDetails.clear();
|
||||
}
|
||||
topologyOptions = fromJS(payload.state.topologyOptions)
|
||||
|| topologyOptions;
|
||||
this.__emitChange();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AppStore(AppDispatcher);
|
||||
@@ -45,6 +45,8 @@ export function getUrlState(state) {
|
||||
pinnedSearches: state.get('pinnedSearches').toJS(),
|
||||
searchQuery: state.get('searchQuery'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
gridSortBy: state.get('gridSortBy'),
|
||||
gridSortedDesc: state.get('gridSortedDesc'),
|
||||
topologyId: state.get('currentTopologyId'),
|
||||
topologyOptions: state.get('topologyOptions').toJS() // all options
|
||||
};
|
||||
|
||||
@@ -63,6 +63,20 @@ export function updateTopologyIds(topologies, parentId) {
|
||||
});
|
||||
}
|
||||
|
||||
export function addTopologyFullname(topologies) {
|
||||
return topologies.map(t => {
|
||||
if (!t.sub_topologies) {
|
||||
return Object.assign({}, t, {fullName: t.name});
|
||||
}
|
||||
return Object.assign({}, t, {
|
||||
fullName: t.name,
|
||||
sub_topologies: t.sub_topologies.map(st => (
|
||||
Object.assign({}, st, {fullName: `${t.name} ${st.name}`})
|
||||
))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// adds ID field to topology (based on last part of URL path) and save urls in
|
||||
// map for easy lookup
|
||||
export function setTopologyUrlsById(topologyUrlsById, topologies) {
|
||||
|
||||
@@ -32,7 +32,7 @@ function maybeUpdate(getState) {
|
||||
receiveNodesDelta(delta);
|
||||
}
|
||||
if (deltaBuffer.size > 0) {
|
||||
updateTimer = setTimeout(maybeUpdate, feedInterval);
|
||||
updateTimer = setTimeout(() => maybeUpdate(getState), feedInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
/* weave company colours */
|
||||
@weave-gray-blue: rgb(85,105,145);
|
||||
@weave-blue: rgb(0,210,255);
|
||||
@weave-blue-transparent: rgb(0,210,255, 0.1);
|
||||
@weave-orange: rgb(255,75,25);
|
||||
@weave-charcoal-blue: rgb(50,50,75); // #32324B
|
||||
|
||||
@@ -273,7 +274,6 @@ h2 {
|
||||
margin-bottom: 3px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
background-color: #f7f7fa;
|
||||
&-active, &:hover {
|
||||
color: @text-color;
|
||||
background-color: @background-darker-secondary-color;
|
||||
@@ -1131,7 +1131,7 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
.topology-option, .metric-selector, .network-selector {
|
||||
.topology-option, .metric-selector, .network-selector, .grid-mode-selector {
|
||||
color: @text-secondary-color;
|
||||
margin: 6px 0;
|
||||
|
||||
@@ -1183,6 +1183,12 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
.grid-mode-selector .fa {
|
||||
margin-right: 4px;
|
||||
margin-left: 0;
|
||||
color: @text-secondary-color;
|
||||
}
|
||||
|
||||
.network-selector-action {
|
||||
border-top: 3px solid transparent;
|
||||
border-bottom: 3px solid @background-dark-color;
|
||||
@@ -1226,9 +1232,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 {
|
||||
@@ -1402,7 +1417,7 @@ h2 {
|
||||
//
|
||||
|
||||
@help-panel-width: 400px;
|
||||
@help-panel-height: 380px;
|
||||
@help-panel-height: 420px;
|
||||
.help-panel {
|
||||
position: absolute;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
@@ -1512,16 +1527,63 @@ h2 {
|
||||
}
|
||||
|
||||
.nodes-grid {
|
||||
|
||||
tr {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&-label-minor {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&-id-column {
|
||||
margin: -3px -4px;
|
||||
padding: 2px 2px;
|
||||
|
||||
.content {
|
||||
padding: 1px 4px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.selected &, &:hover {
|
||||
.content {
|
||||
background-color: #d7ecf5;
|
||||
}
|
||||
}
|
||||
.selected & .content {
|
||||
border: 1px solid @weave-blue;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
.node-details-relatives {
|
||||
color: inherit;
|
||||
font-size: 90%;
|
||||
white-space: normal;
|
||||
opacity: 0.8;
|
||||
line-height: 110%;
|
||||
text-align: right;
|
||||
|
||||
margin-left: 0;
|
||||
// margin-top: 0;
|
||||
// display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
*/
|
||||
|
||||
.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;
|
||||
@@ -1533,15 +1595,56 @@ h2 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.node-details-table-node > * {
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.node-details-table-node, thead tr {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.node-details-table-node {
|
||||
&:hover, &.selected {
|
||||
background-color: @background-darker-color;
|
||||
&.selected, &:hover {
|
||||
background-color: @background-lighter-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
|
||||
table {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
thead {
|
||||
// osx scrollbar width: 0
|
||||
// linux scrollbar width: 16
|
||||
// avg scrollbar width: 8
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
thead, tbody tr {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
tbody:after {
|
||||
content: '';
|
||||
display: block;
|
||||
// height of the controls so you can scroll the last row up above them
|
||||
// and have a good look.
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
thead {
|
||||
box-shadow: 0 4px 2px -2px rgba(0, 0, 0, 0.16);
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: block;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ type Node struct {
|
||||
NodeSummary
|
||||
Controls []ControlInstance `json:"controls"`
|
||||
Children []NodeSummaryGroup `json:"children,omitempty"`
|
||||
Parents []Parent `json:"parents,omitempty"`
|
||||
Connections []ConnectionsSummary `json:"connections,omitempty"`
|
||||
}
|
||||
|
||||
@@ -86,7 +85,6 @@ func MakeNode(topologyID string, r report.Report, ns report.Nodes, n report.Node
|
||||
NodeSummary: summary,
|
||||
Controls: controls(r, n),
|
||||
Children: children(r, n),
|
||||
Parents: Parents(r, n),
|
||||
Connections: []ConnectionsSummary{
|
||||
incomingConnectionsSummary(topologyID, r, n, ns),
|
||||
outgoingConnectionsSummary(topologyID, r, n, ns),
|
||||
|
||||
@@ -218,6 +218,23 @@ func TestMakeDetailedContainerNode(t *testing.T) {
|
||||
Metric: &fixture.ServerContainerMemoryMetric,
|
||||
},
|
||||
},
|
||||
Parents: []detailed.Parent{
|
||||
{
|
||||
ID: expected.ServerContainerImageNodeID,
|
||||
Label: fixture.ServerContainerImageName,
|
||||
TopologyID: "containers-by-image",
|
||||
},
|
||||
{
|
||||
ID: fixture.ServerHostNodeID,
|
||||
Label: fixture.ServerHostName,
|
||||
TopologyID: "hosts",
|
||||
},
|
||||
{
|
||||
ID: fixture.ServerPodNodeID,
|
||||
Label: "pong-b",
|
||||
TopologyID: "pods",
|
||||
},
|
||||
},
|
||||
},
|
||||
Controls: []detailed.ControlInstance{},
|
||||
Children: []detailed.NodeSummaryGroup{
|
||||
@@ -232,23 +249,6 @@ func TestMakeDetailedContainerNode(t *testing.T) {
|
||||
Nodes: []detailed.NodeSummary{serverProcessNodeSummary},
|
||||
},
|
||||
},
|
||||
Parents: []detailed.Parent{
|
||||
{
|
||||
ID: expected.ServerContainerImageNodeID,
|
||||
Label: fixture.ServerContainerImageName,
|
||||
TopologyID: "containers-by-image",
|
||||
},
|
||||
{
|
||||
ID: fixture.ServerHostNodeID,
|
||||
Label: fixture.ServerHostName,
|
||||
TopologyID: "hosts",
|
||||
},
|
||||
{
|
||||
ID: fixture.ServerPodNodeID,
|
||||
Label: "pong-b",
|
||||
TopologyID: "pods",
|
||||
},
|
||||
},
|
||||
Connections: []detailed.ConnectionsSummary{
|
||||
{
|
||||
ID: "incoming-connections",
|
||||
@@ -335,6 +335,18 @@ func TestMakeDetailedPodNode(t *testing.T) {
|
||||
{ID: "container", Label: "# Containers", Value: "1", Priority: 4, Datatype: "number"},
|
||||
{ID: "kubernetes_namespace", Label: "Namespace", Value: "ping", Priority: 5},
|
||||
},
|
||||
Parents: []detailed.Parent{
|
||||
{
|
||||
ID: fixture.ServerHostNodeID,
|
||||
Label: fixture.ServerHostName,
|
||||
TopologyID: "hosts",
|
||||
},
|
||||
{
|
||||
ID: fixture.ServiceNodeID,
|
||||
Label: fixture.ServiceName,
|
||||
TopologyID: "services",
|
||||
},
|
||||
},
|
||||
},
|
||||
Controls: []detailed.ControlInstance{},
|
||||
Children: []detailed.NodeSummaryGroup{
|
||||
@@ -358,18 +370,6 @@ func TestMakeDetailedPodNode(t *testing.T) {
|
||||
Nodes: []detailed.NodeSummary{serverProcessNodeSummary},
|
||||
},
|
||||
},
|
||||
Parents: []detailed.Parent{
|
||||
{
|
||||
ID: fixture.ServerHostNodeID,
|
||||
Label: fixture.ServerHostName,
|
||||
TopologyID: "hosts",
|
||||
},
|
||||
{
|
||||
ID: fixture.ServiceNodeID,
|
||||
Label: fixture.ServiceName,
|
||||
TopologyID: "services",
|
||||
},
|
||||
},
|
||||
Connections: []detailed.ConnectionsSummary{
|
||||
{
|
||||
ID: "incoming-connections",
|
||||
|
||||
@@ -64,6 +64,7 @@ type NodeSummary struct {
|
||||
Linkable bool `json:"linkable,omitempty"` // Whether this node can be linked-to
|
||||
Pseudo bool `json:"pseudo,omitempty"`
|
||||
Metadata []report.MetadataRow `json:"metadata,omitempty"`
|
||||
Parents []Parent `json:"parents,omitempty"`
|
||||
Metrics []report.MetricRow `json:"metrics,omitempty"`
|
||||
Tables []report.Table `json:"tables,omitempty"`
|
||||
Adjacency report.IDList `json:"adjacency,omitempty"`
|
||||
@@ -133,6 +134,7 @@ func baseNodeSummary(r report.Report, n report.Node) NodeSummary {
|
||||
Linkable: true,
|
||||
Metadata: NodeMetadata(r, n),
|
||||
Metrics: NodeMetrics(r, n),
|
||||
Parents: Parents(r, n),
|
||||
Tables: NodeTables(r, n),
|
||||
Adjacency: n.Adjacency.Copy(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user