diff --git a/client/app/scripts/components/nodes-resources.js b/client/app/scripts/components/nodes-resources.js index 6ca74a416a..25e076225f 100644 --- a/client/app/scripts/components/nodes-resources.js +++ b/client/app/scripts/components/nodes-resources.js @@ -2,9 +2,9 @@ import React from 'react'; import { connect } from 'react-redux'; import Logo from './logo'; -import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; import ZoomWrapper from './zoom-wrapper'; import NodesResourcesLayer from './nodes-resources/node-resources-layer'; +import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; class NodesResources extends React.Component { diff --git a/client/app/scripts/components/nodes-resources/node-resources-layer.js b/client/app/scripts/components/nodes-resources/node-resources-layer.js index 1fa26f3f94..af613cac57 100644 --- a/client/app/scripts/components/nodes-resources/node-resources-layer.js +++ b/client/app/scripts/components/nodes-resources/node-resources-layer.js @@ -5,25 +5,24 @@ import { Map as makeMap } from 'immutable'; import NodeResourcesMetricBox from './node-resources-metric-box'; import NodeResourcesLayerTopology from './node-resources-layer-topology'; import { - layersVerticalPositionSelector, - positionedNodesByTopologySelector, -} from '../../selectors/resource-view/layers'; + layerVerticalPositionByTopologyIdSelector, + layoutNodesByTopologyIdSelector, +} from '../../selectors/resource-view/layout'; class NodesResourcesLayer extends React.Component { render() { - const { layerVerticalPosition, topologyId, transform, nodes } = this.props; + const { layerVerticalPosition, topologyId, transform, layoutNodes } = this.props; return ( - {nodes.toIndexedSeq().map(node => ( + {layoutNodes.toIndexedSeq().map(node => ( ))} - {!nodes.isEmpty() && = RESOURCES_LABEL_MIN_SIZE; - const showNode = width >= 1; // px + const showNode = width >= 1; // hide the thin nodes // Don't display the nodes which are less than 1px wide. // TODO: Show `+ 31 nodes` kind of tag in their stead. @@ -82,12 +82,11 @@ class NodeResourcesMetricBox extends React.Component { return ( - {info} - {withCapacity && } + {showCapacity && } {showInfo && m.get('label') === metricType); - if (!metric) return node; - - const { formattedValue } = getMetricValue(metric); - const info = `${metricType} - ${formattedValue}`; - const absoluteConsumption = metric.get('value'); - const withCapacity = node.get('withCapacity'); - const totalCapacity = withCapacity ? metric.get('max') : absoluteConsumption; - const relativeConsumption = absoluteConsumption / totalCapacity; - const format = metric.get('format'); - - return node.set('activeMetric', makeMap({ - totalCapacity, absoluteConsumption, relativeConsumption, withCapacity, info, format - })); -} - +// Decorates the resource node with dimensions taken from its metric summary. export function nodeResourceBoxDecorator(node) { - const widthCriterion = node.get('withCapacity') ? 'totalCapacity' : 'absoluteConsumption'; - const width = node.getIn(['activeMetric', widthCriterion]); + const metricSummary = node.get('metricSummary', makeMap()); + const width = metricSummary.get('showCapacity') ? + metricSummary.get('totalCapacity') : + metricSummary.get('absoluteConsumption'); const height = RESOURCES_LAYER_HEIGHT; return node.merge(makeMap({ width, height })); } -export function nodeParentNodeDecorator(node) { - const parentTopologyId = node.get('directParentTopologyId'); - const parents = node.get('parents', makeMap()); - const parent = parents.find(p => p.get('topologyId') === parentTopologyId); - if (!parent) return node; +// Decorates the node with the summary info of its metric of a fixed type. +export function nodeMetricSummaryDecoratorByType(metricType, showCapacity) { + return (node) => { + const metric = node + .get('metrics', makeMap()) + .find(m => m.get('label') === metricType); + + // Do nothing if there is no metric info. + if (!metric) return node; + + const absoluteConsumption = metric.get('value'); + const totalCapacity = showCapacity ? metric.get('max') : absoluteConsumption; + const relativeConsumption = absoluteConsumption / totalCapacity; + const format = metric.get('format'); + + return node.set('metricSummary', makeMap({ + showCapacity, totalCapacity, absoluteConsumption, relativeConsumption, format + })); + }; +} + +// Decorates the node with the ID of the parent node belonging to a fixed topology. +export function nodeParentDecoratorByTopologyId(topologyId) { + return (node) => { + const parent = node + .get('parents', makeMap()) + .find(p => p.get('topologyId') === topologyId); - return node.set('parentNodeId', parent.get('id')); + return parent ? node.set('parentNodeId', parent.get('id')) : node; + }; } diff --git a/client/app/scripts/selectors/graph-view/default-zoom.js b/client/app/scripts/selectors/graph-view/default-zoom.js index e6521e3e95..039196b0fd 100644 --- a/client/app/scripts/selectors/graph-view/default-zoom.js +++ b/client/app/scripts/selectors/graph-view/default-zoom.js @@ -70,7 +70,7 @@ export const graphZoomLimitsSelector = createSelector( maxScaleSelector, ], (defaultZoom, maxScale) => { - if (defaultZoom.isEmpty()) return makeMap({ minScale: 1, maxScale: 1 }); + if (defaultZoom.isEmpty()) return makeMap(); // We always allow zooming out exactly 5x compared to the initial zoom. const minScale = defaultZoom.get('scaleX') / 5; diff --git a/client/app/scripts/selectors/resource-view/default-zoom.js b/client/app/scripts/selectors/resource-view/default-zoom.js index 2ec356146f..6f0b12b0de 100644 --- a/client/app/scripts/selectors/resource-view/default-zoom.js +++ b/client/app/scripts/selectors/resource-view/default-zoom.js @@ -3,18 +3,29 @@ import { Map as makeMap } from 'immutable'; import { RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from '../canvas'; -import { layersVerticalPositionSelector, positionedNodesByTopologySelector } from './layers'; +import { + layerVerticalPositionByTopologyIdSelector, + layoutNodesByTopologyIdSelector, +} from './layout'; -const resourcesBoundingRectangleSelector = createSelector( +// This is used to determine the maximal zoom factor. +const minNodeWidthSelector = createSelector( + [ + layoutNodesByTopologyIdSelector, + ], + layoutNodes => layoutNodes.flatten(true).map(n => n.get('width')).min() +); + +const resourceNodesBoundingRectangleSelector = createSelector( [ - layersVerticalPositionSelector, - positionedNodesByTopologySelector, + layerVerticalPositionByTopologyIdSelector, + layoutNodesByTopologyIdSelector, ], - (verticalPositions, nodes) => { - if (nodes.size === 0) return null; + (verticalPositions, layoutNodes) => { + if (layoutNodes.size === 0) return null; - const flattenedNodes = nodes.flatten(true); + const flattenedNodes = layoutNodes.flatten(true); const xMin = flattenedNodes.map(n => n.get('offset')).min(); const yMin = verticalPositions.toList().min(); const xMax = flattenedNodes.map(n => n.get('offset') + n.get('width')).max(); @@ -24,10 +35,10 @@ const resourcesBoundingRectangleSelector = createSelector( } ); -// Compute the default zoom settings for the given chart. +// Compute the default zoom settings for given resources. export const resourcesDefaultZoomSelector = createSelector( [ - resourcesBoundingRectangleSelector, + resourceNodesBoundingRectangleSelector, canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector, @@ -37,6 +48,7 @@ export const resourcesDefaultZoomSelector = createSelector( const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS(); + // The default scale takes all the available horizontal space and 70% of the vertical space. const scaleX = (width / (xMax - xMin)) * 1.0; const scaleY = (height / (yMax - yMin)) * 0.7; @@ -53,17 +65,10 @@ export const resourcesDefaultZoomSelector = createSelector( } ); -const minNodeWidthSelector = createSelector( - [ - positionedNodesByTopologySelector, - ], - nodes => nodes.flatten(true).map(n => n.get('width')).min() -); - export const resourcesZoomLimitsSelector = createSelector( [ resourcesDefaultZoomSelector, - resourcesBoundingRectangleSelector, + resourceNodesBoundingRectangleSelector, minNodeWidthSelector, canvasWidthSelector, ], @@ -73,7 +78,9 @@ export const resourcesZoomLimitsSelector = createSelector( const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS(); return makeMap({ + // Maximal zoom is such that the smallest box takes the whole canvas. maxScale: width / minNodeWidth, + // Minimal zoom is equivalent to the initial one, where the whole layout matches the canvas. minScale: defaultZoom.get('scaleX'), minTranslateX: xMin, maxTranslateX: xMax, diff --git a/client/app/scripts/selectors/resource-view/layers.js b/client/app/scripts/selectors/resource-view/layers.js deleted file mode 100644 index 997a533977..0000000000 --- a/client/app/scripts/selectors/resource-view/layers.js +++ /dev/null @@ -1,125 +0,0 @@ -import { times } from 'lodash'; -import { fromJS, Map as makeMap } from 'immutable'; -import { createSelector } from 'reselect'; - -import { RESOURCES_LAYER_PADDING, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; -import { resourceViewLayers, topologiesWithCapacity } from '../../constants/resources'; -import { - nodeResourceViewColorDecorator, - nodeParentNodeDecorator, - nodeResourceBoxDecorator, - nodeActiveMetricDecorator, -} from '../../decorators/node'; - - -const RESOURCE_VIEW_MAX_LAYERS = 3; - -const nodeWeight = node => ( - node.get('withCapacity') ? - -node.getIn(['activeMetric', 'relativeConsumption']) : - -node.get('width') -); - -export const layersTopologyIdsSelector = createSelector( - [ - state => state.get('currentTopologyId'), - ], - topologyId => fromJS(resourceViewLayers[topologyId] || []) -); - -export const layersVerticalPositionSelector = createSelector( - [ - layersTopologyIdsSelector, - ], - (topologiesIds) => { - let yPositions = makeMap(); - let currentY = RESOURCES_LAYER_PADDING; - - topologiesIds.forEach((topologyId) => { - currentY -= RESOURCES_LAYER_HEIGHT + RESOURCES_LAYER_PADDING; - yPositions = yPositions.set(topologyId, currentY); - }); - - return yPositions; - } -); - -const decoratedNodesByTopologySelector = createSelector( - [ - layersTopologyIdsSelector, - state => state.get('pinnedMetricType'), - ...times(RESOURCE_VIEW_MAX_LAYERS, index => ( - state => state.getIn(['nodesByTopology', layersTopologyIdsSelector(state).get(index)]) - )) - ], - (layersTopologyIds, pinnedMetricType, ...topologiesNodes) => { - let nodesByTopology = makeMap(); - let lastLayerTopologyId = null; - - topologiesNodes.forEach((topologyNodes, index) => { - const layerTopologyId = layersTopologyIds.get(index); - const withCapacity = topologiesWithCapacity.includes(layerTopologyId); - const decoratedTopologyNodes = (topologyNodes || makeMap()) - .map(node => node.set('directParentTopologyId', lastLayerTopologyId)) - .map(node => node.set('topologyId', layerTopologyId)) - .map(node => node.set('activeMetricType', pinnedMetricType)) - .map(node => node.set('withCapacity', withCapacity)) - .map(nodeResourceViewColorDecorator) - .map(nodeActiveMetricDecorator) - .map(nodeResourceBoxDecorator) - .map(nodeParentNodeDecorator); - const filteredTopologyNodes = decoratedTopologyNodes - .filter(node => nodesByTopology.hasIn([lastLayerTopologyId, node.get('parentNodeId')]) - || index === 0) - .filter(node => node.get('width')); - - nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes); - lastLayerTopologyId = layerTopologyId; - }); - - return nodesByTopology; - } -); - -export const positionedNodesByTopologySelector = createSelector( - [ - layersTopologyIdsSelector, - decoratedNodesByTopologySelector, - ], - (layersTopologyIds, decoratedNodesByTopology) => { - let result = makeMap(); - - layersTopologyIds.forEach((layerTopologyId, index) => { - const decoratedNodes = decoratedNodesByTopology.get(layerTopologyId, makeMap()); - const buckets = decoratedNodes.groupBy(node => node.get('parentNodeId')); - - buckets.forEach((bucket, parentNodeId) => { - const parentTopologyId = layersTopologyIds.get(index - 1); - let offset = result.getIn([parentTopologyId, parentNodeId, 'offset'], 0); - - bucket.sortBy(nodeWeight).forEach((node, nodeId) => { - const positionedNode = node.set('offset', offset); - result = result.setIn([layerTopologyId, nodeId], positionedNode); - offset += node.get('width'); - }); - - // TODO: Get rid of this disgusting code - const parentOffset = result.getIn([parentTopologyId, parentNodeId, 'offset'], 0); - const parentWidth = result.getIn([parentTopologyId, parentNodeId, 'width'], offset); - const overhead = (offset - parentOffset) / parentWidth; - if (overhead > 1) { - // console.log(overhead); - bucket.forEach((_, nodeId) => { - const node = result.getIn([layerTopologyId, nodeId]); - result = result.mergeIn([layerTopologyId, nodeId], makeMap({ - offset: ((node.get('offset') - parentOffset) / overhead) + parentOffset, - width: node.get('width') / overhead, - })); - }); - } - }); - }); - - return result; - } -); diff --git a/client/app/scripts/selectors/resource-view/layout.js b/client/app/scripts/selectors/resource-view/layout.js new file mode 100644 index 0000000000..021c8f3f30 --- /dev/null +++ b/client/app/scripts/selectors/resource-view/layout.js @@ -0,0 +1,177 @@ +import debug from 'debug'; +import { times } from 'lodash'; +import { fromJS, Map as makeMap } from 'immutable'; +import { createSelector } from 'reselect'; + +import { RESOURCES_LAYER_PADDING, RESOURCES_LAYER_HEIGHT } from '../../constants/styles'; +import { + RESOURCE_VIEW_MAX_LAYERS, + RESOURCE_VIEW_LAYERS, + TOPOLOGIES_WITH_CAPACITY, +} from '../../constants/resources'; +import { + nodeParentDecoratorByTopologyId, + nodeMetricSummaryDecoratorByType, + nodeResourceViewColorDecorator, + nodeResourceBoxDecorator, +} from '../../decorators/node'; + + +const log = debug('scope:nodes-layout'); + +// Used for ordering the resource nodes. +const resourceNodeConsumptionComparator = (node) => { + const metricSummary = node.get('metricSummary'); + return metricSummary.get('showCapacity') ? + -metricSummary.get('relativeConsumption') : + -metricSummary.get('absoluteConsumption'); +}; + +// A list of topologies shown in the resource view of the active topology (bottom to top). +export const layersTopologyIdsSelector = createSelector( + [ + state => state.get('currentTopologyId'), + ], + topologyId => fromJS(RESOURCE_VIEW_LAYERS[topologyId] || []) +); + +// Calculates the resource view layer Y-coordinate for every topology in the resource view. +export const layerVerticalPositionByTopologyIdSelector = createSelector( + [ + layersTopologyIdsSelector, + ], + (topologiesIds) => { + let yPositions = makeMap(); + let currentY = RESOURCES_LAYER_PADDING; + + topologiesIds.forEach((topologyId) => { + currentY -= RESOURCES_LAYER_HEIGHT + RESOURCES_LAYER_PADDING; + yPositions = yPositions.set(topologyId, currentY); + }); + + return yPositions; + } +); + +// Decorate and filter all the nodes to be displayed in the current resource view, except +// for the exact node horizontal offsets which are calculated from the data created here. +const decoratedNodesByTopologySelector = createSelector( + [ + layersTopologyIdsSelector, + state => state.get('pinnedMetricType'), + // Generate the dependencies for this selector programmatically (because we want their + // number to be customizable directly by changing the constant). The dependency functions + // here depend on another selector, but this seems to work quite fine. For example, if + // layersTopologyIdsSelector = ['hosts', 'containers'] and RESOURCE_VIEW_MAX_LAYERS = 3, + // this code will generate: + // [ + // state => state.getIn(['nodesByTopology', 'hosts']) + // state => state.getIn(['nodesByTopology', 'containers']) + // state => state.getIn(['nodesByTopology', undefined]) + // ] + // which will all be captured by `topologiesNodes` and processed correctly (even for undefined). + ...times(RESOURCE_VIEW_MAX_LAYERS, index => ( + state => state.getIn(['nodesByTopology', layersTopologyIdsSelector(state).get(index)]) + )) + ], + (layersTopologyIds, pinnedMetricType, ...topologiesNodes) => { + let nodesByTopology = makeMap(); + let parentLayerTopologyId = null; + + topologiesNodes.forEach((topologyNodes, index) => { + const layerTopologyId = layersTopologyIds.get(index); + const parentTopologyNodes = nodesByTopology.get(parentLayerTopologyId, makeMap()); + const showCapacity = TOPOLOGIES_WITH_CAPACITY.includes(layerTopologyId); + const isBaseLayer = (index === 0); + + const nodeParentDecorator = nodeParentDecoratorByTopologyId(parentLayerTopologyId); + const nodeMetricSummaryDecorator = nodeMetricSummaryDecoratorByType( + pinnedMetricType, showCapacity); + + // Color the node, deduce its anchor point, dimensions and info about its pinned metric. + const decoratedTopologyNodes = (topologyNodes || makeMap()) + .map(nodeResourceViewColorDecorator) + .map(nodeMetricSummaryDecorator) + .map(nodeResourceBoxDecorator) + .map(nodeParentDecorator); + + const filteredTopologyNodes = decoratedTopologyNodes + // Filter out the nodes with no parent in the topology of the previous layer, as their + // positions in the layout could not be determined. The exception is the base layer. + // TODO: Also make an exception for uncontained nodes (e.g. processes). + .filter(node => parentTopologyNodes.has(node.get('parentNodeId')) || isBaseLayer) + // Filter out the nodes with no metric summary data, which is needed to render the node. + .filter(node => node.get('metricSummary')); + + nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes); + parentLayerTopologyId = layerTopologyId; + }); + + return nodesByTopology; + } +); + +// Calculate (and fix) the offsets for all the displayed resource nodes. +export const layoutNodesByTopologyIdSelector = createSelector( + [ + layersTopologyIdsSelector, + decoratedNodesByTopologySelector, + ], + (layersTopologyIds, nodesByTopology) => { + let layoutNodes = makeMap(); + let parentTopologyId = null; + + // Calculate the offsets bottom-to top as each layer needs to know exact offsets of its parents. + layersTopologyIds.forEach((layerTopologyId) => { + let positionedNodes = makeMap(); + + // Get the nodes in the current layer grouped by their parent nodes. + // Each of those buckets will be positioned and sorted independently. + const nodesByParent = nodesByTopology + .get(layerTopologyId, makeMap()) + .groupBy(n => n.get('parentNodeId')); + + nodesByParent.forEach((nodesBucket, parentNodeId) => { + // Set the initial offset to the offset of the parent (that has already been set). + // If there is no offset information, i.e. we're processing the base layer, set it to 0. + const parentNode = layoutNodes.getIn([parentTopologyId, parentNodeId], makeMap()); + let currentOffset = parentNode.get('offset', 0); + + // Sort the nodes in the current bucket and lay them down one after another. + nodesBucket.sortBy(resourceNodeConsumptionComparator).forEach((node, nodeId) => { + const positionedNode = node.set('offset', currentOffset); + positionedNodes = positionedNodes.set(nodeId, positionedNode); + currentOffset += node.get('width'); + }); + + // TODO: This block of code checks for the overlaps which are caused by children + // consuming more resources than their parent node. This happens due to inconsistent + // data being sent from the backend and it needs to be fixed there. + const parentOffset = parentNode.get('offset', 0); + const parentWidth = parentNode.get('width', currentOffset); + const totalChildrenWidth = currentOffset - parentOffset; + // If the total width of the children exceeds the parent node box width, we have a problem. + // We fix it by shrinking all the children to by a factor to perfectly fit into the parent. + if (totalChildrenWidth > parentWidth) { + const shrinkFactor = parentWidth / totalChildrenWidth; + log(`Inconsistent data: Children of ${parentNodeId} reported to use more ` + + `resource than the node itself - shrinking by factor ${shrinkFactor}`); + // Shrink all the children. + nodesBucket.forEach((_, nodeId) => { + const node = positionedNodes.get(nodeId); + positionedNodes = positionedNodes.mergeIn([nodeId], makeMap({ + offset: ((node.get('offset') - parentOffset) * shrinkFactor) + parentOffset, + width: node.get('width') * shrinkFactor, + })); + }); + } + }); + + // Update the layout with the positioned node from the current layer. + layoutNodes = layoutNodes.mergeIn([layerTopologyId], positionedNodes); + parentTopologyId = layerTopologyId; + }); + + return layoutNodes; + } +); diff --git a/client/app/scripts/selectors/topology.js b/client/app/scripts/selectors/topology.js index 40eb621f49..31dce70fa5 100644 --- a/client/app/scripts/selectors/topology.js +++ b/client/app/scripts/selectors/topology.js @@ -31,6 +31,9 @@ export const isResourceViewModeSelector = createSelector( viewMode => viewMode === RESOURCE_VIEW_MODE ); +// This is used by the resource view where we're always taking the nodes from the cache, +// so that polling doesn't affect the layout. Once we implement a more robust polling +// mechanism that could poll multiple topologies at once, we'll be able to get rid of this. export const cachedCurrentTopologyNodesSelector = createSelector( [ state => state.get('nodesByTopology'), @@ -71,7 +74,10 @@ export const activeTopologyZoomCacheKeyPathSelector = createSelector( ], (isGraphViewMode, viewMode, topologyId, pinnedMetricType, topologyOptions) => ( isGraphViewMode ? + // In graph view, selecting different options/filters produces a different layout. ['zoomCache', viewMode, topologyId, topologyOptions] : + // Otherwise we're in the resource view where the options are hidden (for now), + // but pinning different metrics can result in very different layouts. ['zoomCache', viewMode, topologyId, pinnedMetricType] ) ); diff --git a/client/app/scripts/selectors/zooming.js b/client/app/scripts/selectors/zooming.js index 62588bed84..cf011e05d2 100644 --- a/client/app/scripts/selectors/zooming.js +++ b/client/app/scripts/selectors/zooming.js @@ -43,6 +43,7 @@ export const activeLayoutZoomStateSelector = createSelector( ], (isGraphView, graphDefaultZoom, resourcesDefaultZoom, cachedZoomState) => { const defaultZoom = isGraphView ? graphDefaultZoom : resourcesDefaultZoom; + // All the cached fields override the calculated default ones. return defaultZoom.merge(cachedZoomState); } ); diff --git a/client/app/scripts/utils/metric-utils.js b/client/app/scripts/utils/metric-utils.js index f9232a5583..e67852a3f5 100644 --- a/client/app/scripts/utils/metric-utils.js +++ b/client/app/scripts/utils/metric-utils.js @@ -54,6 +54,7 @@ export function getMetricValue(metric) { }; } + export function getMetricColor(metric) { const selectedMetric = metric && metric.get('id'); if (/mem/.test(selectedMetric)) { diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js index de5b0d126d..82cf1f65c8 100644 --- a/client/app/scripts/utils/topology-utils.js +++ b/client/app/scripts/utils/topology-utils.js @@ -134,7 +134,9 @@ export function getCurrentTopologyOptions(state) { } export function isTopologyEmpty(state) { + // Consider a topology in the resource view empty if it has no pinned metric. const resourceViewEmpty = isResourceViewModeSelector(state) && !pinnedMetricSelector(state); + // Otherwise (in graph and table view), we only look at the node count. const nodeCount = state.getIn(['currentTopology', 'stats', 'node_count'], 0); const nodesEmpty = nodeCount === 0 && state.get('nodes').size === 0; return resourceViewEmpty || nodesEmpty; diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index 6469cec0a7..f259ad19cd 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -10,7 +10,7 @@ import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveEr receiveControlSuccess, receiveTopologies, receiveNotFound, receiveNodesForTopology } from '../actions/app-actions'; -import { layersTopologyIdsSelector } from '../selectors/resource-view/layers'; +import { layersTopologyIdsSelector } from '../selectors/resource-view/layout'; import { API_INTERVAL, TOPOLOGY_INTERVAL } from '../constants/timer'; const log = debug('scope:web-api-utils'); @@ -155,6 +155,9 @@ function doRequest(opts) { return reqwest(config); } +/** + * Does a one-time fetch of all the nodes for a custom list of topologies. + */ function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions = makeMap()) { // fetch sequentially getState().get('topologyUrlsById') @@ -171,7 +174,7 @@ function getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions } /** - * Gets nodes for all topologies (for search) + * Gets nodes for all topologies (for search). */ export function getAllNodes(getState, dispatch) { const state = getState(); @@ -180,13 +183,13 @@ export function getAllNodes(getState, dispatch) { getNodesForTopologies(getState, dispatch, topologyIds, topologyOptions); } -// Update the nodes for all topologies that appear in the current resource view. -// NOTE: At the moment we are only getting their one-time snapshot (instead of polling), -// because we intentionally want to keep the resource view layout static. Later on, we -// will probably want to change this. +/** + * One-time update of all the nodes of topologies that appear in the current resource view. + */ export function getResourceViewNodesSnapshot(getState, dispatch) { const topologyIds = layersTopologyIdsSelector(getState()); - // TODO: Find a more elegant way of fetching the topologies information. + // TODO: Replace this with polling once we figure how to make resource view dynamic + // (from the UI point of view, the challenge is to make it stable). setTimeout(() => { getNodesForTopologies(getState, dispatch, topologyIds); }, 100); @@ -214,8 +217,8 @@ export function getTopologies(options, dispatch) { }); } -// TODO: topologyUrl and options are always used for the current topology so they as arguments -// can be replaced by the `state` and then retrieved here internally from selectors. +// TODO: topologyUrl and options are always used for the current topology instead of forwarding +// them, we could just take `state` as argument and then retrieve these internally with selectors. export function getNodesDelta(topologyUrl, options, dispatch, forceReload) { const optionsQuery = buildOptionsQuery(options); // Only recreate websocket if url changed or if forced (weave cloud instance reload);