mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-05 03:01:11 +00:00
Make app-store's topologies object immutable
* refactoring, functionality should be the same * adapted tests
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
jest.dontMock('../nodes-layout');
|
||||
jest.dontMock('../topology-utils');
|
||||
jest.dontMock('../../utils/topology-utils');
|
||||
jest.dontMock('../../constants/naming'); // edge naming: 'source-target'
|
||||
|
||||
import { fromJS, Map } from 'immutable';
|
||||
|
||||
@@ -3,7 +3,7 @@ import debug from 'debug';
|
||||
import { Map as makeMap, Set as ImmSet } from 'immutable';
|
||||
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { updateNodeDegrees } from './topology-utils';
|
||||
import { updateNodeDegrees } from '../utils/topology-utils';
|
||||
|
||||
const log = debug('scope:nodes-layout');
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
export function updateNodeDegrees(nodes, edges) {
|
||||
return nodes.map(node => {
|
||||
const nodeId = node.get('id');
|
||||
const degree = edges.count(edge => {
|
||||
return edge.get('source') === nodeId || edge.get('target') === nodeId;
|
||||
});
|
||||
return node.set('degree', degree);
|
||||
});
|
||||
}
|
||||
@@ -19,10 +19,10 @@ export default class Status extends React.Component {
|
||||
classNames += ' status-loading';
|
||||
showWarningIcon = true;
|
||||
} else if (this.props.topology) {
|
||||
const stats = this.props.topology.stats;
|
||||
text = `${stats.node_count} nodes`;
|
||||
if (stats.filtered_nodes) {
|
||||
text = `${text} (${stats.filtered_nodes} filtered)`;
|
||||
const stats = this.props.topology.get('stats');
|
||||
text = `${stats.get('node_count')} nodes`;
|
||||
if (stats.get('filtered_nodes')) {
|
||||
text = `${text} (${stats.get('filtered_nodes')} filtered)`;
|
||||
}
|
||||
classNames += ' status-stats';
|
||||
showWarningIcon = false;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { clickTopology } from '../actions/app-actions';
|
||||
|
||||
export default class Topologies extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onTopologyClick = this.onTopologyClick.bind(this);
|
||||
@@ -16,8 +16,8 @@ export default class Topologies extends React.Component {
|
||||
}
|
||||
|
||||
renderSubTopology(subTopology) {
|
||||
const isActive = subTopology.name === this.props.currentTopology.name;
|
||||
const topologyId = subTopology.id;
|
||||
const isActive = subTopology === this.props.currentTopology;
|
||||
const topologyId = subTopology.get('id');
|
||||
const title = this.renderTitle(subTopology);
|
||||
const className = isActive ? 'topologies-sub-item topologies-sub-item-active' : 'topologies-sub-item';
|
||||
|
||||
@@ -25,47 +25,43 @@ export default class Topologies extends React.Component {
|
||||
<div className={className} title={title} key={topologyId} rel={topologyId}
|
||||
onClick={this.onTopologyClick}>
|
||||
<div className="topologies-sub-item-label">
|
||||
{subTopology.name}
|
||||
{subTopology.get('name')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle(topology) {
|
||||
return ['Nodes: ' + topology.stats.node_count,
|
||||
'Connections: ' + topology.stats.node_count].join('\n');
|
||||
return ['Nodes: ' + topology.getIn(['stats', 'node_count']),
|
||||
'Connections: ' + topology.getIn(['stats', 'node_count'])].join('\n');
|
||||
}
|
||||
|
||||
renderTopology(topology) {
|
||||
const isActive = topology.name === this.props.currentTopology.name;
|
||||
const isActive = topology === this.props.currentTopology;
|
||||
const className = isActive ? 'topologies-item-main topologies-item-main-active' : 'topologies-item-main';
|
||||
const topologyId = topology.id;
|
||||
const topologyId = topology.get('id');
|
||||
const title = this.renderTitle(topology);
|
||||
|
||||
return (
|
||||
<div className="topologies-item" key={topologyId}>
|
||||
<div className={className} title={title} rel={topologyId} onClick={this.onTopologyClick}>
|
||||
<div className="topologies-item-label">
|
||||
{topology.name}
|
||||
{topology.get('name')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="topologies-sub">
|
||||
{topology.sub_topologies && topology.sub_topologies.map(this.renderSubTopology)}
|
||||
{topology.has('sub_topologies') && topology.get('sub_topologies').map(this.renderSubTopology)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const topologies = _.sortBy(this.props.topologies, function(topology) {
|
||||
return topology.rank;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="topologies">
|
||||
{this.props.currentTopology && topologies.map(function(topology) {
|
||||
return this.renderTopology(topology);
|
||||
}, this)}
|
||||
{this.props.currentTopology && this.props.topologies.map(
|
||||
topology => this.renderTopology(topology)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import TopologyOptionAction from './topology-option-action';
|
||||
|
||||
@@ -21,7 +20,7 @@ export default class TopologyOptions extends React.Component {
|
||||
const actions = [];
|
||||
const activeOptions = this.props.activeOptions;
|
||||
const topologyId = this.props.topologyId;
|
||||
const option = items[0].option;
|
||||
const option = items.first().get('option');
|
||||
|
||||
// find active option value
|
||||
if (activeOptions && activeOptions.has(option)) {
|
||||
@@ -29,18 +28,18 @@ export default class TopologyOptions extends React.Component {
|
||||
} else {
|
||||
// get default value
|
||||
items.forEach(function(item) {
|
||||
if (item.default) {
|
||||
activeValue = item.value;
|
||||
if (item.get('default')) {
|
||||
activeValue = item.get('value');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// render active option as text, add other options as actions
|
||||
items.forEach(function(item) {
|
||||
if (item.value === activeValue) {
|
||||
activeText = item.display;
|
||||
if (item.get('value') === activeValue) {
|
||||
activeText = item.get('display');
|
||||
} else {
|
||||
actions.push(this.renderAction(item.value, item.option, topologyId));
|
||||
actions.push(this.renderAction(item.get('value'), item.get('option'), topologyId));
|
||||
}
|
||||
}, this);
|
||||
|
||||
@@ -55,20 +54,15 @@ export default class TopologyOptions extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = _.sortBy(
|
||||
_.map(this.props.options, function(items, optionId) {
|
||||
_.each(items, function(item) {
|
||||
item.option = optionId;
|
||||
});
|
||||
items.option = optionId;
|
||||
return items;
|
||||
}),
|
||||
'option'
|
||||
);
|
||||
const options = this.props.options.map((items, optionId) => {
|
||||
let itemsMap = items.map(item => item.set('option', optionId));
|
||||
itemsMap = itemsMap.set('option', optionId);
|
||||
return itemsMap;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="topology-options">
|
||||
{options.map(function(items) {
|
||||
{options.toIndexedSeq().map(function(items) {
|
||||
return this.renderOption(items);
|
||||
}, this)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
jest.dontMock('../../utils/topology-utils');
|
||||
jest.dontMock('../../constants/action-types');
|
||||
jest.dontMock('../app-store');
|
||||
|
||||
@@ -160,7 +161,7 @@ describe('AppStore', function() {
|
||||
|
||||
it('init with no topologies', function() {
|
||||
const topos = AppStore.getTopologies();
|
||||
expect(topos.length).toBe(0);
|
||||
expect(topos.size).toBe(0);
|
||||
expect(AppStore.getCurrentTopology()).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -168,20 +169,20 @@ describe('AppStore', function() {
|
||||
registeredCallback(ClickTopologyAction);
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
|
||||
expect(AppStore.getTopologies().length).toBe(2);
|
||||
expect(AppStore.getCurrentTopology().name).toBe('Topo1');
|
||||
expect(AppStore.getTopologies().size).toBe(2);
|
||||
expect(AppStore.getCurrentTopology().get('name')).toBe('Topo1');
|
||||
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1');
|
||||
expect(AppStore.getCurrentTopologyOptions().option1).toBeDefined();
|
||||
expect(AppStore.getCurrentTopologyOptions().get('option1')).toBeDefined();
|
||||
});
|
||||
|
||||
it('get sub-topology', function() {
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
registeredCallback(ClickSubTopologyAction);
|
||||
|
||||
expect(AppStore.getTopologies().length).toBe(2);
|
||||
expect(AppStore.getCurrentTopology().name).toBe('topo 1 grouped');
|
||||
expect(AppStore.getTopologies().size).toBe(2);
|
||||
expect(AppStore.getCurrentTopology().get('name')).toBe('topo 1 grouped');
|
||||
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1-grouped');
|
||||
expect(AppStore.getCurrentTopologyOptions()).toBeUndefined();
|
||||
expect(AppStore.getCurrentTopologyOptions().size).toBe(0);
|
||||
});
|
||||
|
||||
// topology options
|
||||
@@ -402,7 +403,7 @@ describe('AppStore', function() {
|
||||
it('selectes relatives topology while keeping node selected', function() {
|
||||
registeredCallback(ClickTopologyAction);
|
||||
registeredCallback(ReceiveTopologiesAction);
|
||||
expect(AppStore.getCurrentTopology().name).toBe('Topo1');
|
||||
expect(AppStore.getCurrentTopology().get('name')).toBe('Topo1');
|
||||
|
||||
registeredCallback(ClickNodeAction);
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
@@ -422,6 +423,6 @@ describe('AppStore', function() {
|
||||
expect(AppStore.getSelectedNodeId()).toBe('rel1');
|
||||
expect(AppStore.getNodeDetails().keySeq().last()).toEqual('rel1');
|
||||
expect(AppStore.getNodeDetails().size).toEqual(1);
|
||||
expect(AppStore.getCurrentTopology().name).toBe('Topo2');
|
||||
expect(AppStore.getCurrentTopology().get('name')).toBe('Topo2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +1,23 @@
|
||||
import _ from 'lodash';
|
||||
import debug from 'debug';
|
||||
import Immutable from 'immutable';
|
||||
import { fromJS, 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 } from '../utils/topology-utils';
|
||||
|
||||
const makeMap = Immutable.Map;
|
||||
const makeOrderedMap = Immutable.OrderedMap;
|
||||
const makeSet = Immutable.Set;
|
||||
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 findTopologyById(subTree, topologyId) {
|
||||
let foundTopology;
|
||||
|
||||
_.each(subTree, function(topology) {
|
||||
if (_.endsWith(topology.url, topologyId)) {
|
||||
foundTopology = topology;
|
||||
}
|
||||
if (!foundTopology) {
|
||||
foundTopology = findTopologyById(topology.sub_topologies, topologyId);
|
||||
}
|
||||
if (foundTopology) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return foundTopology;
|
||||
}
|
||||
|
||||
function makeNode(node) {
|
||||
return {
|
||||
id: node.id,
|
||||
@@ -64,7 +48,7 @@ let mouseOverNodeId = null;
|
||||
let nodeDetails = makeOrderedMap(); // nodeId -> details
|
||||
let nodes = makeOrderedMap(); // nodeId -> node
|
||||
let selectedNodeId = null;
|
||||
let topologies = [];
|
||||
let topologies = makeList();
|
||||
let topologiesLoaded = false;
|
||||
let topologyUrlsById = makeOrderedMap(); // topologyId -> topologyUrl
|
||||
let routeSet = false;
|
||||
@@ -72,15 +56,19 @@ let controlPipes = makeOrderedMap(); // pipeId -> controlPipe
|
||||
let updatePausedAt = null; // Date
|
||||
let websocketClosed = true;
|
||||
|
||||
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(topologyList) {
|
||||
_.each(topologyList, function(topology) {
|
||||
topology.id = topology.url.split('/').pop();
|
||||
topologyUrlsById = topologyUrlsById.set(topology.id, topology.url);
|
||||
processTopologies(topology.sub_topologies);
|
||||
});
|
||||
return topologyList;
|
||||
function processTopologies(nextTopologies) {
|
||||
// add IDs to topology objects in-place
|
||||
updateTopologyIds(nextTopologies);
|
||||
|
||||
// cache URLs by ID
|
||||
topologyUrlsById = setTopologyUrlsById(topologyUrlsById, nextTopologies);
|
||||
|
||||
const immNextTopologies = fromJS(nextTopologies).sortBy(topologySorter);
|
||||
topologies = topologies.mergeDeep(immNextTopologies);
|
||||
}
|
||||
|
||||
function setTopology(topologyId) {
|
||||
@@ -89,24 +77,28 @@ function setTopology(topologyId) {
|
||||
}
|
||||
|
||||
function setDefaultTopologyOptions(topologyList) {
|
||||
_.each(topologyList, function(topology) {
|
||||
topologyList.forEach(topology => {
|
||||
let defaultOptions = makeOrderedMap();
|
||||
_.each(topology.options, function(items, option) {
|
||||
_.each(items, function(item) {
|
||||
if (item.default === true) {
|
||||
defaultOptions = defaultOptions.set(option, item.value);
|
||||
}
|
||||
if (topology.has('options')) {
|
||||
topology.get('options').forEach(function(items, option) {
|
||||
items.forEach(item => {
|
||||
if (item.get('default') === true) {
|
||||
defaultOptions = defaultOptions.set(option, item.get('value'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultOptions.size) {
|
||||
topologyOptions = topologyOptions.set(
|
||||
topology.id,
|
||||
topology.get('id'),
|
||||
defaultOptions
|
||||
);
|
||||
}
|
||||
|
||||
setDefaultTopologyOptions(topology.sub_topologies);
|
||||
if (topology.has('sub_topologies')) {
|
||||
setDefaultTopologyOptions(topology.get('sub_topologies'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,11 +183,11 @@ export class AppStore extends Store {
|
||||
}
|
||||
|
||||
getCurrentTopologyOptions() {
|
||||
return currentTopology && currentTopology.options;
|
||||
return currentTopology && currentTopology.get('options') || makeOrderedMap();
|
||||
}
|
||||
|
||||
getCurrentTopologyUrl() {
|
||||
return currentTopology && currentTopology.url;
|
||||
return currentTopology && currentTopology.get('url');
|
||||
}
|
||||
|
||||
getErrorUrl() {
|
||||
@@ -289,7 +281,7 @@ export class AppStore extends Store {
|
||||
}
|
||||
|
||||
isTopologyEmpty() {
|
||||
return currentTopology && currentTopology.stats && currentTopology.stats.node_count === 0 && nodes.size === 0;
|
||||
return currentTopology && currentTopology.get('stats') && currentTopology.get('stats').get('node_count') === 0 && nodes.size === 0;
|
||||
}
|
||||
|
||||
isUpdatePaused() {
|
||||
@@ -560,7 +552,7 @@ export class AppStore extends Store {
|
||||
|
||||
// add new nodes
|
||||
_.each(payload.delta.add, function(node) {
|
||||
nodes = nodes.set(node.id, Immutable.fromJS(makeNode(node)));
|
||||
nodes = nodes.set(node.id, fromJS(makeNode(node)));
|
||||
});
|
||||
|
||||
if (emitChange) {
|
||||
@@ -581,7 +573,7 @@ export class AppStore extends Store {
|
||||
case ActionTypes.RECEIVE_TOPOLOGIES:
|
||||
errorUrl = null;
|
||||
topologyUrlsById = topologyUrlsById.clear();
|
||||
topologies = processTopologies(payload.topologies);
|
||||
processTopologies(payload.topologies);
|
||||
setTopology(currentTopologyId);
|
||||
// only set on first load, if options are not already set via route
|
||||
if (!topologiesLoaded && topologyOptions.size === 0) {
|
||||
@@ -619,7 +611,7 @@ export class AppStore extends Store {
|
||||
} else {
|
||||
nodeDetails = nodeDetails.clear();
|
||||
}
|
||||
topologyOptions = Immutable.fromJS(payload.state.topologyOptions)
|
||||
topologyOptions = fromJS(payload.state.topologyOptions)
|
||||
|| topologyOptions;
|
||||
this.__emitChange();
|
||||
break;
|
||||
|
||||
56
client/app/scripts/utils/topology-utils.js
Normal file
56
client/app/scripts/utils/topology-utils.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
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);
|
||||
}
|
||||
if (foundTopology) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return foundTopology;
|
||||
}
|
||||
|
||||
|
||||
export function updateNodeDegrees(nodes, edges) {
|
||||
return nodes.map(node => {
|
||||
const nodeId = node.get('id');
|
||||
const degree = edges.count(edge => {
|
||||
return edge.get('source') === nodeId || edge.get('target') === nodeId;
|
||||
});
|
||||
return node.set('degree', degree);
|
||||
});
|
||||
}
|
||||
|
||||
/* set topology.id in place on each topology */
|
||||
export function updateTopologyIds(topologies) {
|
||||
topologies.forEach(topology => {
|
||||
topology.id = topology.url.split('/').pop();
|
||||
if (topology.sub_topologies) {
|
||||
updateTopologyIds(topology.sub_topologies);
|
||||
}
|
||||
});
|
||||
return topologies;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user