);
}
@@ -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,
};
diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js
index 38ae981c2..e9632af3c 100644
--- a/client/app/scripts/components/nodes.js
+++ b/client/app/scripts/components/nodes.js
@@ -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 (
);
}
@@ -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);
diff --git a/client/app/scripts/components/sidebar.js b/client/app/scripts/components/sidebar.js
index 4a6973c00..4e3f0c641 100644
--- a/client/app/scripts/components/sidebar.js
+++ b/client/app/scripts/components/sidebar.js
@@ -1,8 +1,9 @@
import React from 'react';
-export default function Sidebar({children}) {
+export default function Sidebar({children, classNames}) {
+ const className = `sidebar ${classNames}`;
return (
-
+
{children}
);
diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js
index 72d42793d..4e9ab8c17 100644
--- a/client/app/scripts/constants/action-types.js
+++ b/client/app/scripts/constants/action-types.js
@@ -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);
diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js
index f626c6733..c6f6d1c71 100644
--- a/client/app/scripts/constants/styles.js
+++ b/client/app/scripts/constants/styles.js
@@ -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,
+};
diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js
index 893247774..ba9ef0dce 100644
--- a/client/app/scripts/reducers/root.js
+++ b/client/app/scripts/reducers/root.js
@@ -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);
}
diff --git a/client/app/scripts/utils/router-utils.js b/client/app/scripts/utils/router-utils.js
index 0aba5a157..c69a1f627 100644
--- a/client/app/scripts/utils/router-utils.js
+++ b/client/app/scripts/utils/router-utils.js
@@ -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
};
diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js
index 4da44970a..14c365558 100644
--- a/client/app/scripts/utils/topology-utils.js
+++ b/client/app/scripts/utils/topology-utils.js
@@ -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) {
diff --git a/client/app/scripts/utils/update-buffer-utils.js b/client/app/scripts/utils/update-buffer-utils.js
index 8f628aaea..d77706a70 100644
--- a/client/app/scripts/utils/update-buffer-utils.js
+++ b/client/app/scripts/utils/update-buffer-utils.js
@@ -32,7 +32,7 @@ function maybeUpdate(getState) {
receiveNodesDelta(delta);
}
if (deltaBuffer.size > 0) {
- updateTimer = setTimeout(maybeUpdate, feedInterval);
+ updateTimer = setTimeout(() => maybeUpdate(getState), feedInterval);
}
}
}
diff --git a/client/app/styles/main.less b/client/app/styles/main.less
index f1c19adce..f7d98859f 100644
--- a/client/app/styles/main.less
+++ b/client/app/styles/main.less
@@ -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;
+ }
+ }
+}
diff --git a/render/detailed/node.go b/render/detailed/node.go
index c01a5e73a..fa04b3a2c 100644
--- a/render/detailed/node.go
+++ b/render/detailed/node.go
@@ -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),
diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go
index f314a83ac..9d6659438 100644
--- a/render/detailed/node_test.go
+++ b/render/detailed/node_test.go
@@ -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",
diff --git a/render/detailed/summary.go b/render/detailed/summary.go
index a78f52436..4d176aa7f 100644
--- a/render/detailed/summary.go
+++ b/render/detailed/summary.go
@@ -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(),
}