mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-04 18:51:17 +00:00
* 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
178 lines
7.6 KiB
JavaScript
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;
|
|
}
|
|
);
|