Files
weave-scope/client/app/scripts/utils/topology-utils.js
Roland Schilter 21a0093c08 Keep topology nav visible if selected
If the scope-app API unexpectedly restarts, it has no report
at hand (until it gets one from the probe) and sends node
count 0 to the frontend for all topologies. Once the report
arrives, it will send the proper count.

What happened was the frontend did hide Processes for a short
time till the node count recovered. This moved the topology
selection to the always visible Containers (hide_if_empty == false)
while keeping the graph as is.

Once the node count recovers, Processes comes back but the
selection is still at Containers.

We now keep the selected topology visible at all time even if
the API returns a node count of 0. This recovers nicely when
the correct node counts come in. Once the user selects a different
topology while and a backend response arrives, it disappears.

Fixes #2646
2017-07-12 17:12:43 +02:00

181 lines
5.3 KiB
JavaScript

import { endsWith } from 'lodash';
import { Set as makeSet, List as makeList } from 'immutable';
import { isPausedSelector } from '../selectors/time-travel';
import { isResourceViewModeSelector } from '../selectors/topology';
import { pinnedMetricSelector } from '../selectors/node-metric';
import { shownNodesSelector, shownResourceTopologyIdsSelector } from '../selectors/node-filters';
//
// top priority first
//
const TOPOLOGY_DISPLAY_PRIORITY = [
'ecs-services',
'ecs-tasks',
'kube-controllers',
'services',
'replica-sets',
'pods',
'containers',
];
export function getDefaultTopology(topologies) {
const flatTopologies = topologies
.flatMap(t => makeList([t]).concat(t.get('sub_topologies', makeList())));
return flatTopologies
.sortBy((t) => {
const index = TOPOLOGY_DISPLAY_PRIORITY.indexOf(t.get('id'));
return index === -1 ? Infinity : index;
})
.getIn([0, 'id']);
}
/**
* Returns a cache ID based on the topologyId and optionsQuery
* @param {String} topologyId
* @param {object} topologyOptions (optional)
* @return {String}
*/
export function buildTopologyCacheId(topologyId, topologyOptions) {
let id = '';
if (topologyId) {
id = topologyId;
if (topologyOptions) {
id += JSON.stringify(topologyOptions);
}
}
return id;
}
/**
* Returns a topology object from the topology tree
* @param {List} subTree
* @param {String} topologyId
* @return {Map} topology if found
*/
export function findTopologyById(subTree, topologyId) {
let foundTopology;
subTree.forEach((topology) => {
if (endsWith(topology.get('url'), topologyId)) {
foundTopology = topology;
}
if (!foundTopology && topology.has('sub_topologies')) {
foundTopology = findTopologyById(topology.get('sub_topologies'), topologyId);
}
});
return foundTopology;
}
export function updateNodeDegrees(nodes, edges) {
return nodes.map((node) => {
const nodeId = node.get('id');
const degree = edges.count(edge => edge.get('source') === nodeId
|| edge.get('target') === nodeId);
return node.set('degree', degree);
});
}
/* set topology.id and parentId for sub-topologies in place */
export function updateTopologyIds(topologies, parentId) {
return topologies.map((topology) => {
const result = Object.assign({}, topology);
result.id = topology.url.split('/').pop();
if (parentId) {
result.parentId = parentId;
}
if (topology.sub_topologies) {
result.sub_topologies = updateTopologyIds(topology.sub_topologies, result.id);
}
return result;
});
}
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) {
let urlMap = topologyUrlsById;
if (topologies) {
topologies.forEach((topology) => {
urlMap = urlMap.set(topology.id, topology.url);
if (topology.sub_topologies) {
topology.sub_topologies.forEach((subTopology) => {
urlMap = urlMap.set(subTopology.id, subTopology.url);
});
}
});
}
return urlMap;
}
export function filterHiddenTopologies(topologies, currentTopologyId) {
return topologies.filter(t => (!t.hide_if_empty || t.stats.node_count > 0 ||
t.stats.filtered_nodes > 0 || t.id === currentTopologyId));
}
export function getCurrentTopologyOptions(state) {
return state.getIn(['currentTopology', 'options']);
}
export function isTopologyNodeCountZero(state) {
const nodeCount = state.getIn(['currentTopology', 'stats', 'node_count'], 0);
// If we are browsing the past, assume there would normally be some nodes at different times.
// If we are in the resource view, don't rely on these stats at all (for now).
return nodeCount === 0 && !isPausedSelector(state) && !isResourceViewModeSelector(state);
}
export function isNodesDisplayEmpty(state) {
// Consider a topology in the resource view empty if it has no pinned metric.
if (isResourceViewModeSelector(state)) {
return !pinnedMetricSelector(state) || shownResourceTopologyIdsSelector(state).isEmpty();
}
// Otherwise (in graph and table view), we only look at the nodes content.
return shownNodesSelector(state).isEmpty();
}
export function getAdjacentNodes(state, originNodeId) {
let adjacentNodes = makeSet();
const nodeId = originNodeId || state.get('selectedNodeId');
if (nodeId) {
if (state.hasIn(['nodes', nodeId])) {
adjacentNodes = makeSet(state.getIn(['nodes', nodeId, 'adjacency']));
// fill up set with reverse edges
state.get('nodes').forEach((node, id) => {
if (node.get('adjacency') && node.get('adjacency').includes(nodeId)) {
adjacentNodes = adjacentNodes.add(id);
}
});
}
}
return adjacentNodes;
}
export function hasSelectedNode(state) {
const selectedNodeId = state.get('selectedNodeId');
return state.hasIn(['nodes', selectedNodeId]);
}
export function getCurrentTopologyUrl(state) {
return state.getIn(['currentTopology', 'url']);
}