Files
weave-scope/client/app/scripts/selectors/chartSelectors.js
2016-12-15 15:04:53 +01:00

128 lines
3.7 KiB
JavaScript

import debug from 'debug';
import { identity } from 'lodash';
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';
import { Map as makeMap, is, Set } from 'immutable';
import { getAdjacentNodes } from '../utils/topology-utils';
import { getSearchableFields } from '../utils/search-utils';
const log = debug('scope:selectors');
//
// `mergeDeepKeyIntersection` does a deep merge on keys that exists in both maps
//
function mergeDeepKeyIntersection(mapA, mapB) {
const commonKeys = Set.fromKeys(mapA).intersect(mapB.keySeq());
return makeMap(commonKeys.map(k => [k, mapA.get(k).mergeDeep(mapB.get(k))]));
}
//
// `returnPreviousRefIfEqual` is a helper function that checks the new computed of a selector
// against the previously computed value. If they are deeply equal return the previous result. This
// is important for things like connect() which tests whether componentWillReceiveProps should be
// called by doing a '===' on the values you return from mapStateToProps.
//
// e.g.
//
// const filteredThings = createSelector(
// state => state.things,
// (things) => things.filter(t => t > 2)
// );
//
// // This will trigger componentWillReceiveProps on every store change:
// connect(s => { things: filteredThings(s) }, ThingComponent);
//
// // But if we wrap it, the result will be === if it `is()` equal and...
// const filteredThingsWrapped = returnPreviousRefIfEqual(filteredThings);
//
// // ...We're safe!
// connect(s => { things: filteredThingsWrapped(s) }, ThingComponent);
//
// Note: This is a slightly strange way to use reselect. Selectors memoize their *arguments* not
// "their results", so use the result of the wrapped selector as the argument to another selector
// here to memoize it and get what we want.
//
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, is);
const returnPreviousRefIfEqual = selector => createDeepEqualSelector(selector, identity);
//
// Selectors!
//
const allNodesSelector = state => state.get('nodes');
export const nodesSelector = returnPreviousRefIfEqual(
createSelector(
allNodesSelector,
allNodes => allNodes.filter(node => !node.get('filtered'))
)
);
export const adjacentNodesSelector = returnPreviousRefIfEqual(getAdjacentNodes);
export const nodeAdjacenciesSelector = returnPreviousRefIfEqual(
createSelector(
nodesSelector,
nodes => nodes.map(n => makeMap({
id: n.get('id'),
adjacency: n.get('adjacency'),
}))
)
);
export const dataNodesSelector = createSelector(
nodesSelector,
nodes => nodes.map((node, id) => makeMap({
id,
label: node.get('label'),
pseudo: node.get('pseudo'),
subLabel: node.get('labelMinor'),
nodeCount: node.get('node_count'),
metrics: node.get('metrics'),
rank: node.get('rank'),
shape: node.get('shape'),
stack: node.get('stack'),
networks: node.get('networks'),
}))
);
export const searchableFieldsSelector = returnPreviousRefIfEqual(
createSelector(
allNodesSelector,
getSearchableFields
)
);
//
// FIXME: this is a bit of a hack...
//
export const layoutNodesSelector = (_, props) => props.layoutNodes || makeMap();
export const completeNodesSelector = createSelector(
layoutNodesSelector,
dataNodesSelector,
(layoutNodes, dataNodes) => {
//
// There are no guarantees whether this selector will be computed first (when
// node-chart-elements.mapStateToProps is called by store.subscribe before
// nodes-chart.mapStateToProps is called), and component render batching and yadada.
//
if (layoutNodes.size !== dataNodes.size) {
log('Obviously mismatched node data', layoutNodes.size, dataNodes.size);
}
return mergeDeepKeyIntersection(dataNodes, layoutNodes);
}
);