Files
weave-scope/client/app/scripts/selectors/resource-view/layout.js
Filip Barl 69fd397217 Initial version of the resource view (#2296)
* Added resource view selector button

* Showing resource boxes in the resource view

* Crude CPU resource view prototype

* Improved the viewMode state logic

* Extracted zooming into a separate wrapper component

* Split the layout selectors between graph-view and resource-view

* Proper zooming logic for the resource view

* Moved all node networks utils to selectors

* Improved the zoom caching logic

* Further refactoring of selectors

* Added sticky labels to the resource boxes

* Added panning translation limits in the resource view

* Renamed GridModeSelector -> ViewModeSelector

* Polished the topology resource view selection logic

* Search bar hidden in the resource view

* Added per-layer topology names to the resource view

* Made metric selectors work for the resource view

* Adjusted the viewport selectors

* Renamed viewport selector to canvas (+ maximal zoom fix)

* Showing more useful metric info in the resource box labels

* Fetching only necessary nodes for the resource view

* Refactored the resource view layer component

* Addressed first batch UI comments (from the Scope meeting)

* Switch to deep zooming transform in the resource view to avoid SVG precision errors

* Renamed and moved resource view components

* Polished all the resource view components

* Changing the available metrics selection

* Improved and polished the state transition logic for the resource view

* Separated zoom limits from the zoom active state

* Renaming and bunch of comments

* Addressed all the UI comments (@davkal + @fons)

* Made graph view selectors independent from resource view selectors
2017-03-24 14:51:53 +01:00

178 lines
7.6 KiB
JavaScript

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;
}
);