- {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 (
+ title={header.label} key={header.id}>
{isSortedAsc
&& }
{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 (
-
-
+
+
{headers}
-
+
{nodes && nodes.map(node => (
))}
@@ -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,
};
diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js
index 6b8fef680..cac1b184b 100644
--- a/client/app/scripts/components/nodes.js
+++ b/client/app/scripts/components/nodes.js
@@ -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} />
{this.renderEmptyTopologyError(topologiesLoaded && nodesLoaded && topologyEmpty)}
- {this.props.gridMode ?
+ {gridMode ?
:
}
@@ -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')
};
}
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/styles.js b/client/app/scripts/constants/styles.js
index 0a232277f..c6f6d1c71 100644
--- a/client/app/scripts/constants/styles.js
+++ b/client/app/scripts/constants/styles.js
@@ -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,
diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js
index e2ad25ea0..e69a3b511 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');
@@ -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);
}
diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js
deleted file mode 100644
index eea6372e7..000000000
--- a/client/app/scripts/stores/app-store.js
+++ /dev/null
@@ -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);
diff --git a/client/app/scripts/utils/router-utils.js b/client/app/scripts/utils/router-utils.js
index 7ed77ee20..86bef1cb1 100644
--- a/client/app/scripts/utils/router-utils.js
+++ b/client/app/scripts/utils/router-utils.js
@@ -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
};
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 f2a963f1d..7e0fdc920 100644
--- a/client/app/styles/main.less
+++ b/client/app/styles/main.less
@@ -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;
+ }
}
}
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(),
}
|