mirror of
https://github.com/weaveworks/scope.git
synced 2026-08-19 04:16:21 +00:00
@@ -18,10 +18,12 @@ export function showHelp() {
|
||||
return {type: ActionTypes.SHOW_HELP};
|
||||
}
|
||||
|
||||
|
||||
export function hideHelp() {
|
||||
return {type: ActionTypes.HIDE_HELP};
|
||||
}
|
||||
|
||||
|
||||
export function toggleHelp() {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().get('showingHelp')) {
|
||||
@@ -32,6 +34,18 @@ export function toggleHelp() {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function sortOrderChanged(sortBy, sortedDesc) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ActionTypes.SORT_ORDER_CHANGED,
|
||||
sortBy, sortedDesc
|
||||
});
|
||||
updateRoute(getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Networks
|
||||
//
|
||||
@@ -83,6 +97,7 @@ export function unpinNetwork(networkId) {
|
||||
// Metrics
|
||||
//
|
||||
|
||||
|
||||
export function selectMetric(metricId) {
|
||||
return {
|
||||
type: ActionTypes.SELECT_METRIC,
|
||||
@@ -217,6 +232,22 @@ export function clickForceRelayout() {
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleGridMode(enabledArgument) {
|
||||
return (dispatch, getState) => {
|
||||
const enabled = (enabledArgument === undefined) ?
|
||||
!getState().get('gridMode') :
|
||||
enabledArgument;
|
||||
dispatch({
|
||||
type: ActionTypes.SET_GRID_MODE,
|
||||
enabled
|
||||
});
|
||||
updateRoute(getState);
|
||||
if (!enabled) {
|
||||
dispatch(clickForceRelayout());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function clickNode(nodeId, label, origin) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
|
||||
@@ -17,18 +17,12 @@ import { getActiveTopologyOptions, getAdjacentNodes,
|
||||
|
||||
const log = debug('scope:nodes-chart');
|
||||
|
||||
const MARGINS = {
|
||||
top: 130,
|
||||
left: 40,
|
||||
right: 40,
|
||||
bottom: 0
|
||||
};
|
||||
|
||||
const ZOOM_CACHE_FIELDS = ['scale', 'panTranslateX', 'panTranslateY'];
|
||||
|
||||
// make sure circular layouts a bit denser with 3-6 nodes
|
||||
const radiusDensity = d3.scale.threshold()
|
||||
.domain([3, 6]).range([2.5, 3.5, 3]);
|
||||
.domain([3, 6])
|
||||
.range([2.5, 3.5, 3]);
|
||||
|
||||
class NodesChart extends React.Component {
|
||||
|
||||
@@ -47,8 +41,8 @@ class NodesChart extends React.Component {
|
||||
scale: 1,
|
||||
selectedNodeScale: d3.scale.linear(),
|
||||
hasZoomed: false,
|
||||
height: 0,
|
||||
width: 0,
|
||||
height: props.height || 0,
|
||||
width: props.width || 0,
|
||||
zoomCache: {}
|
||||
};
|
||||
}
|
||||
@@ -237,9 +231,9 @@ class NodesChart extends React.Component {
|
||||
// move origin node to center of viewport
|
||||
const zoomScale = state.scale;
|
||||
const translate = [state.panTranslateX, state.panTranslateY];
|
||||
const centerX = (-translate[0] + (state.width + MARGINS.left
|
||||
const centerX = (-translate[0] + (state.width + props.margins.left
|
||||
- DETAILS_PANEL_WIDTH) / 2) / zoomScale;
|
||||
const centerY = (-translate[1] + (state.height + MARGINS.top) / 2) / zoomScale;
|
||||
const centerY = (-translate[1] + (state.height + props.margins.top) / 2) / zoomScale;
|
||||
stateNodes = stateNodes.mergeIn([props.selectedNodeId], {
|
||||
x: centerX,
|
||||
y: centerY
|
||||
@@ -310,9 +304,7 @@ class NodesChart extends React.Component {
|
||||
}
|
||||
|
||||
updateGraphState(props, state) {
|
||||
const n = props.nodes.size;
|
||||
|
||||
if (n === 0) {
|
||||
if (props.nodes.size === 0) {
|
||||
return {
|
||||
nodes: makeMap(),
|
||||
edges: makeMap()
|
||||
@@ -328,7 +320,7 @@ class NodesChart extends React.Component {
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
scale: nodeScale,
|
||||
margins: MARGINS,
|
||||
margins: props.margins,
|
||||
forceRelayout: props.forceRelayout,
|
||||
topologyId: this.props.topologyId,
|
||||
topologyOptions: this.props.topologyOptions
|
||||
@@ -353,12 +345,12 @@ class NodesChart extends React.Component {
|
||||
.map(edge => edge.set('ppoints', edge.get('points')));
|
||||
|
||||
// adjust layout based on viewport
|
||||
const xFactor = (state.width - MARGINS.left - MARGINS.right) / graph.width;
|
||||
const xFactor = (state.width - props.margins.left - props.margins.right) / graph.width;
|
||||
const yFactor = state.height / graph.height;
|
||||
const zoomFactor = Math.min(xFactor, yFactor);
|
||||
let zoomScale = this.state.scale;
|
||||
|
||||
if (!state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
|
||||
if (this.zoom && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
|
||||
zoomScale = zoomFactor;
|
||||
// saving in d3's behavior cache
|
||||
this.zoom.scale(zoomFactor);
|
||||
@@ -402,7 +394,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)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/* eslint react/jsx-no-bind: "off", no-multi-comp: "off" */
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { List as makeList, Map as makeMap } from 'immutable';
|
||||
import NodeDetailsTable from '../components/node-details/node-details-table';
|
||||
import { clickNode, sortOrderChanged } from '../actions/app-actions';
|
||||
|
||||
import { getNodeColor } from '../utils/color-utils';
|
||||
|
||||
|
||||
const IGNORED_COLUMNS = ['docker_container_ports', 'docker_container_id', 'docker_image_id',
|
||||
'docker_container_command', 'docker_container_networks'];
|
||||
|
||||
|
||||
function getColumns(nodes) {
|
||||
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, metricColumns).toJS();
|
||||
}
|
||||
|
||||
|
||||
function renderIdCell(props) {
|
||||
const iconStyle = {
|
||||
width: 16,
|
||||
flex: 'none',
|
||||
color: getNodeColor(props.rank, props.label_major)
|
||||
};
|
||||
const showSubLabel = Boolean(props.pseudo);
|
||||
|
||||
return (
|
||||
<div title={props.label} className="nodes-grid-id-column">
|
||||
<div style={iconStyle}><i className="fa fa-square" /></div>
|
||||
<div className="truncate">
|
||||
{props.label} {showSubLabel &&
|
||||
<span className="nodes-grid-label-minor">{props.label_minor}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
class NodesGrid extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.onClickRow = this.onClickRow.bind(this);
|
||||
this.onSortChange = this.onSortChange.bind(this);
|
||||
}
|
||||
|
||||
onClickRow(ev, node, el) {
|
||||
// TODO: do this better
|
||||
if (ev.target.className === 'node-details-table-node-link') {
|
||||
return;
|
||||
}
|
||||
this.props.clickNode(node.id, node.label, el.getBoundingClientRect());
|
||||
}
|
||||
|
||||
onSortChange(sortBy, sortedDesc) {
|
||||
this.props.sortOrderChanged(sortBy, sortedDesc);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { margins, nodes, height, gridSortBy, gridSortedDesc,
|
||||
searchNodeMatches = makeMap(), searchQuery } = this.props;
|
||||
const cmpStyle = {
|
||||
height,
|
||||
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: this.props.currentTopology && this.props.currentTopology.get('fullName'),
|
||||
id: '',
|
||||
nodes: nodes
|
||||
.toList()
|
||||
.filter(n => !searchQuery || searchNodeMatches.has(n.get('id')))
|
||||
.toJS(),
|
||||
columns: getColumns(nodes)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="nodes-grid">
|
||||
{nodes.size > 0 && <NodeDetailsTable
|
||||
style={cmpStyle}
|
||||
className={className}
|
||||
renderIdCell={renderIdCell}
|
||||
tbodyStyle={tbodyStyle}
|
||||
topologyId={this.props.currentTopologyId}
|
||||
onSortChange={this.onSortChange}
|
||||
onClickRow={this.onClickRow}
|
||||
sortBy={gridSortBy}
|
||||
sortedDesc={gridSortedDesc}
|
||||
selectedNodeId={this.props.selectedNodeId}
|
||||
limit={1000}
|
||||
{...detailsData}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
gridSortBy: state.get('gridSortBy'),
|
||||
gridSortedDesc: state.get('gridSortedDesc'),
|
||||
currentTopology: state.get('currentTopology'),
|
||||
currentTopologyId: state.get('currentTopologyId'),
|
||||
searchNodeMatches: state.getIn(['searchNodeMatches', state.get('currentTopologyId')]),
|
||||
searchQuery: state.get('searchQuery'),
|
||||
selectedNodeId: state.get('selectedNodeId')
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ clickNode, sortOrderChanged }
|
||||
)(NodesGrid);
|
||||
@@ -12,9 +12,10 @@ 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';
|
||||
import MetricSelector from './metric-selector';
|
||||
import NetworkSelector from './networks-selector';
|
||||
import EmbeddedTerminal from './embedded-terminal';
|
||||
@@ -86,6 +87,8 @@ class App extends React.Component {
|
||||
dispatch(pinNextMetric(-1));
|
||||
} else if (char === '>') {
|
||||
dispatch(pinNextMetric(1));
|
||||
} else if (char === 't' || char === 'g') {
|
||||
dispatch(toggleGridMode());
|
||||
} else if (char === 'q') {
|
||||
dispatch(unpinMetric());
|
||||
dispatch(selectMetric(null));
|
||||
@@ -99,8 +102,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 (
|
||||
@@ -125,10 +128,11 @@ class App extends React.Component {
|
||||
|
||||
<Nodes />
|
||||
|
||||
<Sidebar>
|
||||
<Sidebar classNames={gridMode ? 'sidebar-gridmode' : ''}>
|
||||
{showingMetricsSelector && !gridMode && <MetricSelector />}
|
||||
{showingNetworkSelector && !gridMode && <NetworkSelector />}
|
||||
<GridModeSelector />
|
||||
<Status />
|
||||
{showingMetricsSelector && <MetricSelector />}
|
||||
{showingNetworkSelector && <NetworkSelector />}
|
||||
<TopologyOptions />
|
||||
</Sidebar>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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', 'Graph', !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: 'Toggle Table mode'},
|
||||
{key: 'g', label: 'Toggle Graph mode'},
|
||||
];
|
||||
|
||||
const CANVAS_METRIC_SHORTCUTS = [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Map as makeMap } from 'immutable';
|
||||
@@ -48,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 (
|
||||
@@ -123,12 +122,6 @@ export class NodeDetails extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
const key = _.snakeCase(table.title);
|
||||
return (<NodeDetailsTable title={table.title} key={key} rows={table.rows}
|
||||
isNumeric={table.numeric} />);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.notFound) {
|
||||
return this.renderNotAvailable();
|
||||
@@ -236,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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import NodeDetailsTableNodeLink from './node-details-table-node-link';
|
||||
import NodeDetailsTableNodeMetric from './node-details-table-node-metric';
|
||||
|
||||
|
||||
function getValuesForNode(node) {
|
||||
const values = {};
|
||||
['metrics', 'metadata'].forEach(collection => {
|
||||
if (node[collection]) {
|
||||
node[collection].forEach(field => {
|
||||
const result = Object.assign({}, field);
|
||||
result.valueType = collection;
|
||||
values[field.id] = result;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
(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 = [], columnWidths = []) {
|
||||
const fields = getValuesForNode(node);
|
||||
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>
|
||||
);
|
||||
}
|
||||
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" style={style} key={id} />;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default class NodeDetailsTableRow extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
//
|
||||
// We watch how far the mouse moves when click on a row, move to much and we assume that the
|
||||
// user is selecting some data in the row. In this case don't trigger the onClick event which
|
||||
// is most likely a details panel popping open.
|
||||
//
|
||||
this.mouseDragOrigin = [0, 0];
|
||||
|
||||
this.storeLabelRef = this.storeLabelRef.bind(this);
|
||||
this.onMouseDown = this.onMouseDown.bind(this);
|
||||
this.onMouseUp = this.onMouseUp.bind(this);
|
||||
this.onMouseEnter = this.onMouseEnter.bind(this);
|
||||
this.onMouseLeave = this.onMouseLeave.bind(this);
|
||||
}
|
||||
|
||||
storeLabelRef(ref) {
|
||||
this.labelEl = ref;
|
||||
}
|
||||
|
||||
onMouseEnter() {
|
||||
const { node, onMouseEnterRow } = this.props;
|
||||
onMouseEnterRow(node);
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
const { node, onMouseLeaveRow } = this.props;
|
||||
onMouseLeaveRow(node);
|
||||
}
|
||||
|
||||
onMouseDown(ev) {
|
||||
const { pageX, pageY } = ev;
|
||||
this.mouseDragOrigin = [pageX, pageY];
|
||||
}
|
||||
|
||||
onMouseUp(ev) {
|
||||
const [originX, originY] = this.mouseDragOrigin;
|
||||
const { pageX, pageY } = ev;
|
||||
const thresholdPx = 2;
|
||||
const movedTheMouseTooMuch = (
|
||||
Math.abs(originX - pageX) > thresholdPx ||
|
||||
Math.abs(originY - pageY) > thresholdPx
|
||||
);
|
||||
if (movedTheMouseTooMuch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { node, onClick } = this.props;
|
||||
onClick(ev, node, ReactDOM.findDOMNode(this.labelEl));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node, nodeIdKey, topologyId, columns, onClick, 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
|
||||
onMouseDown={onClick && this.onMouseDown}
|
||||
onMouseUp={onClick && this.onMouseUp}
|
||||
onMouseEnter={onMouseEnterRow && this.onMouseEnter}
|
||||
onMouseLeave={onMouseLeaveRow && this.onMouseLeave}
|
||||
className={className}>
|
||||
<td ref={this.storeLabelRef} 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,34 +1,142 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ShowMore from '../show-more';
|
||||
import NodeDetailsTableNodeLink from './node-details-table-node-link';
|
||||
import NodeDetailsTableNodeMetric from './node-details-table-node-metric';
|
||||
import NodeDetailsTableRow from './node-details-table-row';
|
||||
|
||||
|
||||
function isNumberField(field) {
|
||||
return field.dataType && field.dataType === 'number';
|
||||
}
|
||||
|
||||
const CW = {
|
||||
S: '44px',
|
||||
M: '80px',
|
||||
L: '140px',
|
||||
XL: '170px',
|
||||
};
|
||||
|
||||
const COLUMN_WIDTHS = {
|
||||
count: '70px',
|
||||
docker_container_created: CW.L,
|
||||
docker_container_restart_count: CW.M,
|
||||
docker_container_state_human: CW.XL,
|
||||
docker_container_uptime: '85px',
|
||||
docker_cpu_total_usage: CW.M,
|
||||
docker_memory_usage: CW.M,
|
||||
open_files_count: CW.M,
|
||||
pid: CW.M,
|
||||
port: '44px',
|
||||
count: '70px'
|
||||
ppid: CW.M,
|
||||
process_cpu_usage_percent: CW.M,
|
||||
process_memory_usage_bytes: CW.M,
|
||||
threads: CW.M,
|
||||
};
|
||||
|
||||
|
||||
function getDefaultSortBy(columns, nodes) {
|
||||
// default sorter specified by columns
|
||||
const defaultSortColumn = _.find(columns, {defaultSort: true});
|
||||
if (defaultSortColumn) {
|
||||
return defaultSortColumn.id;
|
||||
}
|
||||
// otherwise choose first metric
|
||||
return _.get(nodes, [0, 'metrics', 0, 'id']);
|
||||
}
|
||||
|
||||
|
||||
function getValueForSortBy(sortBy) {
|
||||
// return the node's value based on the sortBy field
|
||||
return (node) => {
|
||||
if (sortBy !== null) {
|
||||
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);
|
||||
}
|
||||
return field.value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function getMetaDataSorters(nodes) {
|
||||
// returns an array of sorters that will take a node
|
||||
return _.get(nodes, [0, 'metadata'], []).map((field, index) => node => {
|
||||
const nodeMetadataField = node.metadata && node.metadata[index];
|
||||
if (nodeMetadataField) {
|
||||
if (isNumberField(nodeMetadataField)) {
|
||||
return parseFloat(nodeMetadataField.value);
|
||||
}
|
||||
return nodeMetadataField.value;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getSortedNodes(nodes, columns, sortBy, sortedDesc) {
|
||||
const sortedNodes = _.sortBy(
|
||||
nodes,
|
||||
getValueForSortBy(sortBy || getDefaultSortBy(columns, nodes)),
|
||||
'label',
|
||||
getMetaDataSorters(nodes)
|
||||
);
|
||||
if (sortedDesc) {
|
||||
sortedNodes.reverse();
|
||||
}
|
||||
return sortedNodes;
|
||||
}
|
||||
|
||||
|
||||
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%';
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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) {
|
||||
super(props, context);
|
||||
this.DEFAULT_LIMIT = 5;
|
||||
this.state = {
|
||||
limit: this.DEFAULT_LIMIT,
|
||||
sortedDesc: true,
|
||||
sortBy: null
|
||||
limit: props.limit || this.DEFAULT_LIMIT,
|
||||
sortedDesc: this.props.sortedDesc,
|
||||
sortBy: this.props.sortBy
|
||||
};
|
||||
this.handleLimitClick = this.handleLimitClick.bind(this);
|
||||
this.getValueForSortBy = this.getValueForSortBy.bind(this);
|
||||
}
|
||||
|
||||
handleHeaderClick(ev, headerId) {
|
||||
@@ -37,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() {
|
||||
@@ -44,109 +153,42 @@ export default class NodeDetailsTable extends React.Component {
|
||||
this.setState({limit});
|
||||
}
|
||||
|
||||
getDefaultSortBy() {
|
||||
// default sorter specified by columns
|
||||
const defaultSortColumn = _.find(this.props.columns, {defaultSort: true});
|
||||
if (defaultSortColumn) {
|
||||
return defaultSortColumn.id;
|
||||
}
|
||||
// otherwise choose first metric
|
||||
return _.get(this.props.nodes, [0, 'metrics', 0, 'id']);
|
||||
}
|
||||
|
||||
getMetaDataSorters() {
|
||||
// returns an array of sorters that will take a node
|
||||
return _.get(this.props.nodes, [0, 'metadata'], []).map((field, index) => node => {
|
||||
const nodeMetadataField = node.metadata[index];
|
||||
if (nodeMetadataField) {
|
||||
if (isNumberField(nodeMetadataField)) {
|
||||
return parseFloat(nodeMetadataField.value);
|
||||
}
|
||||
return nodeMetadataField.value;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
getValueForSortBy(node) {
|
||||
// return the node's value based on the sortBy field
|
||||
const sortBy = this.state.sortBy || this.getDefaultSortBy();
|
||||
if (sortBy !== null) {
|
||||
const field = _.union(node.metrics, node.metadata).find(f => f.id === sortBy);
|
||||
if (field) {
|
||||
if (isNumberField(field)) {
|
||||
return parseFloat(field.value);
|
||||
}
|
||||
return field.value;
|
||||
}
|
||||
}
|
||||
return -1e-10; // just under 0 to treat missing values differently from 0
|
||||
}
|
||||
|
||||
getValuesForNode(node) {
|
||||
const values = {};
|
||||
['metrics', 'metadata'].forEach(collection => {
|
||||
if (node[collection]) {
|
||||
node[collection].forEach(field => {
|
||||
const result = Object.assign({}, field);
|
||||
result.valueType = collection;
|
||||
values[field.id] = result;
|
||||
});
|
||||
}
|
||||
});
|
||||
return values;
|
||||
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 = this.getDefaultSortBy();
|
||||
|
||||
// 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[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
|
||||
@@ -161,66 +203,53 @@ export default class NodeDetailsTable extends React.Component {
|
||||
return '';
|
||||
}
|
||||
|
||||
renderValues(node) {
|
||||
const fields = this.getValuesForNode(node);
|
||||
const columns = this.props.columns || [];
|
||||
return columns.map(({id}) => {
|
||||
const field = fields[id];
|
||||
if (field) {
|
||||
if (field.valueType === 'metadata') {
|
||||
return (
|
||||
<td className="node-details-table-node-value truncate" title={field.value}
|
||||
key={field.id}>
|
||||
{field.value}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
return <NodeDetailsTableNodeMetric key={field.id} {...field} />;
|
||||
}
|
||||
// empty cell to complete the row for proper hover
|
||||
return <td className="node-details-table-node-value" key={id} />;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const headers = this.renderHeaders();
|
||||
const { nodeIdKey } = this.props;
|
||||
let nodes = _.sortBy(this.props.nodes, this.getValueForSortBy, 'label',
|
||||
this.getMetaDataSorters());
|
||||
const { nodeIdKey, columns, topologyId, onClickRow, 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;
|
||||
const expanded = this.state.limit === 0;
|
||||
const notShown = nodes.length - this.DEFAULT_LIMIT;
|
||||
if (this.state.sortedDesc) {
|
||||
nodes.reverse();
|
||||
}
|
||||
const notShown = nodes.length - this.state.limit;
|
||||
if (nodes && limited) {
|
||||
nodes = nodes.slice(0, this.state.limit);
|
||||
}
|
||||
|
||||
const className = classNames('node-details-table-wrapper-wrapper', this.props.className);
|
||||
|
||||
return (
|
||||
<div className="node-details-table-wrapper">
|
||||
<table className="node-details-table">
|
||||
<thead>
|
||||
{headers}
|
||||
</thead>
|
||||
<tbody>
|
||||
{nodes && nodes.map(node => {
|
||||
const values = this.renderValues(node);
|
||||
const nodeId = node[nodeIdKey];
|
||||
return (
|
||||
<tr className="node-details-table-node" key={node.id}>
|
||||
<td className="node-details-table-node-label truncate">
|
||||
<NodeDetailsTableNodeLink {...node} topologyId={this.props.topologyId}
|
||||
nodeId={nodeId} />
|
||||
</td>
|
||||
{values}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<ShowMore handleClick={this.handleLimitClick} collection={this.props.nodes}
|
||||
expanded={expanded} notShown={notShown} />
|
||||
<div className={className}
|
||||
style={this.props.style}>
|
||||
<div className="node-details-table-wrapper">
|
||||
<table className="node-details-table">
|
||||
<thead>
|
||||
{headers}
|
||||
</thead>
|
||||
<tbody style={this.props.tbodyStyle} onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}>
|
||||
{nodes && nodes.map(node => (
|
||||
<NodeDetailsTableRow
|
||||
key={node.id}
|
||||
renderIdCell={this.props.renderIdCell}
|
||||
selected={this.props.selectedNodeId === node.id}
|
||||
node={node}
|
||||
nodeIdKey={nodeIdKey}
|
||||
widths={getColumnsWidths(this.getColumnHeaders())}
|
||||
columns={columns}
|
||||
onClick={onClickRow}
|
||||
onMouseLeaveRow={onMouseLeaveRow}
|
||||
onMouseEnterRow={onMouseEnterRow}
|
||||
topologyId={topologyId} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<ShowMore
|
||||
handleClick={this.handleLimitClick}
|
||||
collection={this.props.nodes}
|
||||
expanded={expanded}
|
||||
notShown={notShown} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -228,5 +257,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,
|
||||
};
|
||||
|
||||
@@ -2,14 +2,15 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import NodesChart from '../charts/nodes-chart';
|
||||
import NodesGrid from '../charts/nodes-grid';
|
||||
import NodesError from '../charts/nodes-error';
|
||||
import { DelayedShow } from '../utils/delayed-show';
|
||||
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;
|
||||
const detailsWidth = 450;
|
||||
|
||||
|
||||
/**
|
||||
@@ -66,25 +67,31 @@ class Nodes extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nodes, selectedNodeId, topologyEmpty, topologiesLoaded, nodesLoaded, topologies,
|
||||
topology } = this.props;
|
||||
const { nodes, topologyEmpty, gridMode, topologiesLoaded, nodesLoaded, topologies,
|
||||
currentTopology } = this.props;
|
||||
const layoutPrecision = getLayoutPrecision(nodes.size);
|
||||
const hasSelectedNode = selectedNodeId && nodes.has(selectedNodeId);
|
||||
|
||||
return (
|
||||
<div className="nodes-wrapper">
|
||||
<DelayedShow delay={1000} show={!topologiesLoaded || (topologiesLoaded && !nodesLoaded)}>
|
||||
<Loading itemType="topologies" show={!topologiesLoaded} />
|
||||
<Loading
|
||||
itemType={getNodeType(topology, topologies)}
|
||||
itemType={getNodeType(currentTopology, topologies)}
|
||||
show={topologiesLoaded && !nodesLoaded} />
|
||||
</DelayedShow>
|
||||
{this.renderEmptyTopologyError(topologiesLoaded && nodesLoaded && topologyEmpty)}
|
||||
<NodesChart {...this.state}
|
||||
detailsWidth={detailsWidth}
|
||||
layoutPrecision={layoutPrecision}
|
||||
hasSelectedNode={hasSelectedNode}
|
||||
/>
|
||||
|
||||
{gridMode ?
|
||||
<NodesGrid {...this.state}
|
||||
nodeSize="24"
|
||||
nodes={nodes}
|
||||
margins={CANVAS_MARGINS}
|
||||
/> :
|
||||
<NodesChart {...this.state}
|
||||
nodes={nodes}
|
||||
margins={CANVAS_MARGINS}
|
||||
layoutPrecision={layoutPrecision}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -101,18 +108,20 @@ class Nodes extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
nodes: state.get('nodes'),
|
||||
currentTopology: state.get('currentTopology'),
|
||||
gridMode: state.get('gridMode'),
|
||||
nodes: state.get('nodes').filter(node => !node.get('filtered')),
|
||||
nodesLoaded: state.get('nodesLoaded'),
|
||||
selectedNodeId: state.get('selectedNodeId'),
|
||||
topologies: state.get('topologies'),
|
||||
topologiesLoaded: state.get('topologiesLoaded'),
|
||||
topologyEmpty: isTopologyEmpty(state),
|
||||
topology: state.get('currentTopology'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(Nodes);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -55,6 +55,8 @@ const ACTION_TYPES = [
|
||||
'UNPIN_NETWORK',
|
||||
'SHOW_NETWORKS',
|
||||
'SET_RECEIVED_NODES_DELTA',
|
||||
'SORT_ORDER_CHANGED',
|
||||
'SET_GRID_MODE',
|
||||
];
|
||||
|
||||
export default _.zipObject(ACTION_TYPES, ACTION_TYPES);
|
||||
|
||||
@@ -10,3 +10,10 @@ export const DETAILS_PANEL_MARGINS = {
|
||||
export const DETAILS_PANEL_OFFSET = 8;
|
||||
|
||||
export const CANVAS_METRIC_FONT_SIZE = 0.19;
|
||||
|
||||
export const CANVAS_MARGINS = {
|
||||
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');
|
||||
@@ -28,6 +28,9 @@ export const initialState = makeMap({
|
||||
currentTopologyId: 'containers',
|
||||
errorUrl: null,
|
||||
forceRelayout: false,
|
||||
gridMode: false,
|
||||
gridSortBy: null,
|
||||
gridSortedDesc: true,
|
||||
highlightedEdgeIds: makeSet(),
|
||||
highlightedNodeIds: makeSet(),
|
||||
hostname: '...',
|
||||
@@ -79,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);
|
||||
}
|
||||
|
||||
@@ -166,6 +170,17 @@ 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);
|
||||
}
|
||||
|
||||
case ActionTypes.CLEAR_CONTROL_ERROR: {
|
||||
return state.removeIn(['controlStatus', action.nodeId, 'error']);
|
||||
}
|
||||
@@ -625,6 +640,13 @@ export function rootReducer(state = initialState, action) {
|
||||
selectedNodeId: action.state.selectedNodeId,
|
||||
pinnedMetricType: action.state.pinnedMetricType
|
||||
});
|
||||
state = state.set('gridMode', action.state.topologyViewMode === '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);
|
||||
}
|
||||
|
||||
@@ -39,11 +39,14 @@ export function getUrlState(state) {
|
||||
|
||||
const urlState = {
|
||||
controlPipe: cp ? cp.toJS() : null,
|
||||
topologyViewMode: state.get('gridMode') ? 'grid' : 'topo',
|
||||
nodeDetails: nodeDetails.toJS(),
|
||||
pinnedMetricType: state.get('pinnedMetricType'),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+129
-6
@@ -269,7 +269,7 @@ h2 {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: @border-radius;
|
||||
opacity: 0.8;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 3px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
@@ -298,6 +298,7 @@ h2 {
|
||||
|
||||
&-error, &-loading {
|
||||
.hideable;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
@@ -889,7 +890,7 @@ h2 {
|
||||
font-size: 105%;
|
||||
line-height: 1.5;
|
||||
|
||||
&:hover {
|
||||
&:hover, &.selected {
|
||||
background-color: lighten(@background-color, 5%);
|
||||
}
|
||||
|
||||
@@ -1130,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;
|
||||
|
||||
@@ -1182,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;
|
||||
@@ -1225,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 {
|
||||
@@ -1401,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);
|
||||
@@ -1490,3 +1506,110 @@ h2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Nodes grid.
|
||||
//
|
||||
|
||||
.nodes-grid {
|
||||
|
||||
tr {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&-label-minor {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&-id-column {
|
||||
margin: -3px -4px;
|
||||
padding: 2px 4px;
|
||||
display: flex;
|
||||
div {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.node-details-table-wrapper-wrapper {
|
||||
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
|
||||
.node-details-table-wrapper {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nodes-grid-graph {
|
||||
position: relative;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.node-details-table-node > * {
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.node-details-table-node, thead tr {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background: @background-color;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tbody tr.selected, tbody tr:hover {
|
||||
background-color: #d7ecf5;
|
||||
border: 1px solid @weave-blue;
|
||||
}
|
||||
|
||||
tbody tr.selected {
|
||||
// box-shadow: 0 4px 2px -2px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
.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