immutable#fromJS JavaScript Examples

The following examples show how to use immutable#fromJS. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: router-utils.js    From ThreatMapper with Apache License 2.0 7 votes vote down vote up
function omitDefaultValues(urlState) {
  // A couple of cases which require special handling because their URL state
  // default values might be in different format than their Redux defaults.
  if (!urlState.controlPipe) {
    urlState = omit(urlState, 'controlPipe');
  }
  if (isEmpty(urlState.nodeDetails)) {
    urlState = omit(urlState, 'nodeDetails');
  }
  if (isEmpty(urlState.topologyOptions)) {
    urlState = omit(urlState, 'topologyOptions');
  }

  // Omit all the fields which match their initial Redux state values.
  return omitBy(urlState, (value, key) => (
    isDeepEqual(fromJS(value), initialRootState.get(key))
  ));
}
Example #2
Source File: layouter-utils-test.js    From ThreatMapper with Apache License 2.0 7 votes vote down vote up
describe('LayouterUtils', () => {
  describe('initEdgesFromNodes', () => {
    it('should return map of edges', () => {
      const input = fromJS({
        a: { adjacency: ['b', 'c'] },
        b: { adjacency: ['a', 'b'] },
        c: {}
      });
      expect(initEdgesFromNodes(input).toJS()).toEqual({
        [edge('a', 'b')]: {
          id: edge('a', 'b'), source: 'a', target: 'b', value: 1
        },
        [edge('a', 'c')]: {
          id: edge('a', 'c'), source: 'a', target: 'c', value: 1
        },
        [edge('b', 'a')]: {
          id: edge('b', 'a'), source: 'b', target: 'a', value: 1
        },
        [edge('b', 'b')]: {
          id: edge('b', 'b'), source: 'b', target: 'b', value: 1
        },
      });
    });
  });
});
Example #3
Source File: index.js    From strapi-plugin-config-sync with MIT License 6 votes vote down vote up
export default function configReducer(state = initialState, action) {
  switch (action.type) {
    case SET_CONFIG_DIFF_IN_STATE:
      return state
        .update('configDiff', () => fromJS(action.config));
    case SET_CONFIG_PARTIAL_DIFF_IN_STATE:
      return state
        .update('partialDiff', () => fromJS(action.config));
    case SET_LOADING_STATE:
      return state
        .update('isLoading', () => fromJS(action.value));
    case SET_APP_ENV_IN_STATE:
      return state
        .update('appEnv', () => fromJS(action.value));
    default:
      return state;
  }
}
Example #4
Source File: node-metric.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
nodeMetricSelector = createMapSelector(
  [
    state => state.get('nodes'),
    selectedMetricIdSelector,
    topCardNodeSelector,
  ],
  (node, selectedMetricId, topCardNode) => {
    const isHighlighted = topCardNode && topCardNode.details && topCardNode.id === node.get('id');
    const sourceNode = isHighlighted ? fromJS(topCardNode.details) : node;
    return sourceNode.get('metrics') && sourceNode.get('metrics')
      .filter(m => m.get('id') === selectedMetricId)
      .first();
  }
)
Example #5
Source File: node-metric.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
availableMetricsSelector = createSelector(
  [
    isGraphViewModeSelector,
    isResourceViewModeSelector,
    state => state.get('nodes'),
  ],
  (isGraphView, isResourceView, nodes) => {
    // In graph view, we always look through the fresh state
    // of topology nodes to get all the available metrics.
    if (isGraphView) {
      return nodes
        .valueSeq()
        .flatMap(n => n.get('metrics', makeList()))
        .filter(m => !m.get('valueEmpty'))
        .map(m => makeMap({ id: m.get('id'), label: m.get('label') }))
        .toSet()
        .toList()
        .sortBy(m => m.get('label'));
    }

    // In resource view, we're displaying only the hardcoded CPU and Memory metrics.
    // TODO: Make this dynamic as well.
    if (isResourceView) {
      return fromJS(RESOURCE_VIEW_METRICS);
    }

    // Don't show any metrics in the table view mode.
    return makeList();
  }
)
Example #6
Source File: node-networks.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
nodeNetworksSelector = createMapSelector(
  [
    state => state.get('nodes'),
  ],
  (node) => {
    const metadata = node.get('metadata', makeList());
    const networks = metadata.find(f => f.get('id') === NETWORKS_ID) || makeMap();
    const networkValues = networks.has('value') ? networks.get('value').split(', ') : [];

    return fromJS(networkValues.map(network => ({
      colorKey: network, id: network, label: network
    })));
  }
)
Example #7
Source File: layout.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
layoutEdgesSelector = createSelector(
  [
    graphEdgesSelector,
    layoutNodesSelector,
    focusedNodesIdsSelector,
  ],
  (graphEdges, layoutNodes, focusedNodesIds) => (
    // Update the edges in the circular layout to link the nodes in a straight line.
    graphEdges.map((edge) => {
      const source = edge.get('source');
      const target = edge.get('target');
      if (includes(focusedNodesIds, source) || includes(focusedNodesIds, target)) {
        return edge.set('points', fromJS([
          pick(layoutNodes.get(source).toJS(), ['x', 'y']),
          pick(layoutNodes.get(target).toJS(), ['x', 'y']),
        ]));
      }
      return edge;
    })
  )
)
Example #8
Source File: node-networks.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
selectedNetworkNodesIdsSelector = createSelector(
  [
    nodeNetworksSelector,
    state => state.get('selectedNetwork'),
  ],
  (nodeNetworks, selectedNetworkId) => {
    const nodeIds = [];
    nodeNetworks.forEach((networks, nodeId) => {
      const networksIds = networks.map(n => n.get('id'));
      if (networksIds.contains(selectedNetworkId)) {
        nodeIds.push(nodeId);
      }
    });
    return fromJS(nodeIds);
  }
)
Example #9
Source File: root.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
// adds ID field to topology (based on last part of URL path) and save urls in
// map for easy lookup
function processTopologies(state, nextTopologies) {
  // add IDs to topology objects in-place
  const topologiesWithId = updateTopologyIds(nextTopologies);
  // filter out hidden topos
  const visibleTopologies = filterHiddenTopologies(topologiesWithId, state.get('currentTopology'));
  // set `selectType` field for topology and sub_topologies options (recursive).
  const topologiesWithSelectType = visibleTopologies.map(calcSelectType);
  // cache URLs by ID
  state = state.set(
    'topologyUrlsById',
    setTopologyUrlsById(state.get('topologyUrlsById'), topologiesWithSelectType)
  );

  const topologiesWithFullnames = addTopologyFullname(topologiesWithSelectType);
  const immNextTopologies = fromJS(topologiesWithFullnames).sortBy(topologySorter);
  return state.set('topologies', immNextTopologies);
}
Example #10
Source File: math-utils-test.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
describe('MathUtils', () => {
  const MathUtils = require('../math-utils');

  describe('modulo', () => {
    const f = MathUtils.modulo;

    it('it should calculate the modulo (also for negatives)', () => {
      expect(f(5, 5)).toBe(0);
      expect(f(4, 5)).toBe(4);
      expect(f(3, 5)).toBe(3);
      expect(f(2, 5)).toBe(2);
      expect(f(1, 5)).toBe(1);
      expect(f(0, 5)).toBe(0);
      expect(f(-1, 5)).toBe(4);
      expect(f(-2, 5)).toBe(3);
      expect(f(-3, 5)).toBe(2);
      expect(f(-4, 5)).toBe(1);
      expect(f(-5, 5)).toBe(0);
    });
  });

  describe('minEuclideanDistanceBetweenPoints', () => {
    const f = MathUtils.minEuclideanDistanceBetweenPoints;
    const entryA = { pointA: { x: 0, y: 0 } };
    const entryB = { pointB: { x: 30, y: 0 } };
    const entryC = { pointC: { x: 0, y: -40 } };
    const entryD = { pointD: { x: -1000, y: 567 } };
    const entryE = { pointE: { x: -999, y: 567 } };
    const entryF = { pointF: { x: 30, y: 0 } };

    it('it should return the minimal distance between any two points in the collection', () => {
      expect(f(fromJS({}))).toBe(Infinity);
      expect(f(fromJS({...entryA}))).toBe(Infinity);
      expect(f(fromJS({...entryA, ...entryB}))).toBe(30);
      expect(f(fromJS({...entryA, ...entryC}))).toBe(40);
      expect(f(fromJS({...entryB, ...entryC}))).toBe(50);
      expect(f(fromJS({
        ...entryA, ...entryB, ...entryC, ...entryD
      }))).toBe(30);
      expect(f(fromJS({
        ...entryA, ...entryB, ...entryC, ...entryD, ...entryE
      }))).toBe(1);
      expect(f(fromJS({
        ...entryA, ...entryB, ...entryC, ...entryD, ...entryF
      }))).toBe(0);
    });
  });
});
Example #11
Source File: debug-toolbar.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
function addMetrics(availableMetrics, node, v) {
  const metrics = availableMetrics.size > 0 ? availableMetrics : fromJS([
    { id: 'host_cpu_usage_percent', label: 'CPU' }
  ]);

  return Object.assign({}, node, {
    metrics: metrics.map(m => Object.assign({}, m, {
      id: 'zing', label: 'zing', max: 100, value: v
    })).toJS()
  });
}
Example #12
Source File: nodes-layout.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
/**
 * Adds `points` array to edge based on location of source and target
 * @param {Map} edge           new edge
 * @param {Map} nodeCache      all nodes
 * @returns {Map}              modified edge
 */
function setSimpleEdgePoints(edge, nodeCache) {
  const source = nodeCache.get(edge.get('source'));
  const target = nodeCache.get(edge.get('target'));
  return edge.set('points', fromJS([
    {x: source.get('x'), y: source.get('y')},
    {x: target.get('x'), y: target.get('y')}
  ]));
}
Example #13
Source File: nodes-layout.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
// Adds some additional waypoints to the edge to make sure the it connects the node
// centers and that the edge enters the target node relatively straight so that the
// arrow is drawn correctly. The total number of waypoints is capped to EDGE_WAYPOINTS_CAP.
function correctedEdgePath(waypoints, source, target) {
  // Get the relevant waypoints that will be added/replicated.
  const sourcePoint = fromJS({ x: source.get('x'), y: source.get('y') });
  const targetPoint = fromJS({ x: target.get('x'), y: target.get('y') });
  const entrancePoint = waypoints.last();

  if (target !== source) {
    // The strategy for the non-loop edges is the following:
    //   * Uniformly select at most CAP - 4 of the central waypoints ignoring the target node
    //     entrance point. Such a selection will ensure that both the source node exit point and
    //     the point before the target node entrance point are taken as boundaries of the interval.
    //   * Now manually add those 4 points that we always want to have included in the edge path -
    //     centers of source/target nodes and twice the target node entrance point to ensure the
    //     edge path actually goes through it and thus doesn't miss the arrow element.
    //   * In the end, what matters for the arrow is that the last 4 points of the array are always
    //     fixed regardless of the total number of waypoints. That way we ensure the arrow is drawn
    //     correctly, but also that the edge path enters the target node smoothly.
    waypoints = fromJS(uniformSelect(waypoints.butLast().toJS(), EDGE_WAYPOINTS_CAP - 4));
    waypoints = waypoints.unshift(sourcePoint);
    waypoints = waypoints.push(entrancePoint);
    waypoints = waypoints.push(entrancePoint);
    waypoints = waypoints.push(targetPoint);
  } else {
    // For loops we simply set the endpoints at the center of source/target node to
    // make them smoother and, of course, we cap the total number of waypoints.
    waypoints = fromJS(uniformSelect(waypoints.toJS(), EDGE_WAYPOINTS_CAP));
    waypoints = waypoints.set(0, sourcePoint);
    waypoints = waypoints.set(waypoints.size - 1, targetPoint);
  }

  return waypoints;
}
Example #14
Source File: edge-container.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
render() {
    const {
      isAnimated, waypoints, scale, ...forwardedProps
    } = this.props;
    const { thickness, waypointsMap } = this.state;

    if (!isAnimated) {
      return transformedEdge(forwardedProps, waypoints.toJS(), thickness);
    }

    return (
      // For the Motion interpolation to work, the waypoints need to be in a map format like
      // { x0: 11, y0: 22, x1: 33, y1: 44 } that we convert to the array format when rendering.
      <Motion style={{
        interpolatedThickness: weakSpring(thickness),
        ...waypointsMap.toJS(),
      }}>
        {
          ({ interpolatedThickness, ...interpolatedWaypoints }) => transformedEdge(
            forwardedProps,
            waypointsMapToArray(fromJS(interpolatedWaypoints)),
            interpolatedThickness
          )
        }
      </Motion>
    );
  }
Example #15
Source File: multi-cloud-action.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
actionOptionsIndex = fromJS({
  start_vulnerability_scan: {
    label: 'Start vulnerability scan',
    onClick: (param, triggerModal) => triggerCVEScanModal(param, triggerModal),
    enabled: false,
  },
  stop_vulnerability_scan: {
    label: 'Stop vulnerability scan',
    onClick: (param, triggerModal, dispatch) =>
      triggerStopCVEScanModal(param, triggerModal, dispatch),
    enabled: false,
  },
  start_secrets_scan: {
    label: 'Start secrets scan',
    onClick: (param, triggerModal, dispatch) =>
      triggerStartSecretsScanModal(param, triggerModal, dispatch),
    enabled: false,
  },
})
Example #16
Source File: request-actions.js    From ThreatMapper with Apache License 2.0 6 votes vote down vote up
export function receiveApiDetails(apiDetails) {
  return (dispatch, getState) => {
    const isFirstTime = !getState().get('version');
    const pausedAt = getState().get('pausedAt');

    dispatch({
      capabilities: fromJS(apiDetails.capabilities || {}),
      hostname: apiDetails.hostname,
      newVersion: apiDetails.newVersion,
      plugins: apiDetails.plugins,
      type: ActionTypes.RECEIVE_API_DETAILS,
      version: apiDetails.version,
    });

    // On initial load either start time travelling at the pausedAt timestamp
    // (if it was given as URL param) if time travelling is enabled, otherwise
    // simply pause at the present time which is arguably the next best thing
    // we could do.
    // NOTE: We can't make this decision before API details are received because
    // we have no prior info on whether time travel would be available.
    if (isFirstTime && pausedAt) {
      if (apiDetails.capabilities && apiDetails.capabilities.historic_reports) {
        dispatch(jumpToTime(pausedAt));
      } else {
        dispatch(pauseTimeAtNow());
      }
    }
  };
}
Example #17
Source File: index.js    From strapi-plugin-sitemap with MIT License 6 votes vote down vote up
initialState = fromJS({
  info: {},
  allowedFields: {},
  settings: Map({}),
  contentTypes: {},
  languages: [],
  initialData: Map({}),
  modifiedContentTypes: Map({}),
  modifiedCustomEntries: Map({}),
})
Example #18
Source File: customHTML2Content.js    From spring-boot-ecommerce with Apache License 2.0 6 votes vote down vote up
createContentBlock = function createContentBlock(blockData, contentState) {
    var key = blockData.key,
        type = blockData.type,
        text = blockData.text,
        data = blockData.data,
        inlineStyles = blockData.inlineStyles,
        entityData = blockData.entityData;

    var blockSpec = {
        type: type != null ? type : 'unstyled',
        text: text != null ? text : '',
        key: key != null ? key : genKey(),
        data: null,
        characterList: List([])
    };
    if (data) {
        blockSpec.data = fromJS(data);
    }
    if (inlineStyles || entityData) {
        var entityKey = void 0;
        if (entityData) {
            var _type = entityData.type,
                mutability = entityData.mutability,
                _data = entityData.data;

            contentState.createEntity(_type, mutability, _data);
            entityKey = contentState.getLastCreatedEntityKey();
        } else {
            entityKey = null;
        }
        var style = OrderedSet(inlineStyles || []);
        var charData = CharacterMetadata.create({ style: style, entityKey: entityKey });
        blockSpec.characterList = List(Repeat(charData, text.length));
    }
    return new ContentBlock(blockSpec);
}
Example #19
Source File: search-utils-test.js    From ThreatMapper with Apache License 2.0 5 votes vote down vote up
describe('SearchUtils', () => {
  const nodeSets = {
    someNodes: fromJS({
      n1: {
        id: 'n1',
        label: 'node label 1',
        metadata: [{
          id: 'fieldId1',
          label: 'Label 1',
          value: 'value 1'
        }],
        metrics: [{
          id: 'metric1',
          label: 'Metric 1',
          value: 1
        }]
      },
      n2: {
        id: 'n2',
        label: 'node label 2',
        metadata: [{
          id: 'fieldId2',
          label: 'Label 2',
          value: 'value 2'
        }],
        tables: [{
          id: 'metric1',
          rows: [{
            entries: {
              label: 'Label 1',
              value: 'Label Value 1'
            },
            id: 'label1'
          }, {
            entries: {
              label: 'Label 2',
              value: 'Label Value 2'
            },
            id: 'label2'
          }],
          type: 'property-list'
        }, {
          columns: [{
            id: 'a',
            label: 'A'
          }, {
            id: 'c',
            label: 'C'
          }],
          id: 'metric2',
          rows: [{
            entries: {
              a: 'xxxa',
              b: 'yyya',
              c: 'zzz1'
            },
            id: 'row1'
          }, {
            entries: {
              a: 'yyyb',
              b: 'xxxb',
              c: 'zzz2'
            },
            id: 'row2'
          }, {
            entries: {
              a: 'Value 1',
              b: 'Value 2',
              c: 'Value 3'
            },
            id: 'row3'
          }],
          type: 'multicolumn-table'
        }],
      },
    })
  };

  describe('applyPinnedSearches', () => {
    const fun = SearchUtils.applyPinnedSearches;

    it('should not filter anything when no pinned searches present', () => {
      let nextState = fromJS({
        nodes: nodeSets.someNodes,
        pinnedSearches: []
      });
      nextState = fun(nextState);
      expect(nextState.get('nodes').filter(node => node.get('filtered')).size).toEqual(0);
    });

    it('should filter nodes if nothing matches a pinned search', () => {
      let nextState = fromJS({
        nodes: nodeSets.someNodes,
        pinnedSearches: ['cantmatch']
      });
      nextState = fun(nextState);
      expect(nextState.get('nodes').filterNot(node => node.get('filtered')).size).toEqual(0);
    });

    it('should filter nodes if nothing matches a combination of pinned searches', () => {
      let nextState = fromJS({
        nodes: nodeSets.someNodes,
        pinnedSearches: ['node label 1', 'node label 2']
      });
      nextState = fun(nextState);
      expect(nextState.get('nodes').filterNot(node => node.get('filtered')).size).toEqual(0);
    });

    it('should filter nodes that do not match a pinned searches', () => {
      let nextState = fromJS({
        nodes: nodeSets.someNodes,
        pinnedSearches: ['Label Value 1']
      });
      nextState = fun(nextState);
      expect(nextState.get('nodes').filter(node => node.get('filtered')).size).toEqual(1);
    });
  });

  describe('findNodeMatch', () => {
    const fun = SearchUtils.findNodeMatch;

    it('does not add a non-matching field', () => {
      let matches = fromJS({});
      matches = fun(
        matches, ['node1', 'field1'],
        'some value', 'some query', null, 'some label'
      );
      expect(matches.size).toBe(0);
    });

    it('adds a matching field', () => {
      let matches = fromJS({});
      matches = fun(
        matches, ['node1', 'field1'],
        'samevalue', 'samevalue', null, 'some label'
      );
      expect(matches.size).toBe(1);
      expect(matches.getIn(['node1', 'field1'])).toBeDefined();
      const {
        text, label, start, length
      } = matches.getIn(['node1', 'field1']);
      expect(text).toBe('samevalue');
      expect(label).toBe('some label');
      expect(start).toBe(0);
      expect(length).toBe(9);
    });

    it('does not add a field when the prefix does not match the label', () => {
      let matches = fromJS({});
      matches = fun(
        matches, ['node1', 'field1'],
        'samevalue', 'samevalue', 'some prefix', 'some label'
      );
      expect(matches.size).toBe(0);
    });

    it('adds a field when the prefix matches the label', () => {
      let matches = fromJS({});
      matches = fun(
        matches, ['node1', 'field1'],
        'samevalue', 'samevalue', 'prefix', 'prefixed label'
      );
      expect(matches.size).toBe(1);
    });
  });

  describe('findNodeMatchMetric', () => {
    const fun = SearchUtils.findNodeMatchMetric;

    it('does not add a non-matching field', () => {
      let matches = fromJS({});
      matches = fun(
        matches, ['node1', 'field1'],
        1, 'metric1', 'metric2', 'lt', 2
      );
      expect(matches.size).toBe(0);
    });

    it('adds a matching field', () => {
      let matches = fromJS({});
      matches = fun(
        matches, ['node1', 'field1'],
        1, 'metric1', 'metric1', 'lt', 2
      );
      expect(matches.size).toBe(1);
      expect(matches.getIn(['node1', 'field1'])).toBeDefined();
      const { metric } = matches.getIn(['node1', 'field1']);
      expect(metric).toBeTruthy();

      matches = fun(
        matches, ['node2', 'field1'],
        1, 'metric1', 'metric1', 'gt', 0
      );
      expect(matches.size).toBe(2);

      matches = fun(
        matches, ['node3', 'field1'],
        1, 'metric1', 'metric1', 'eq', 1
      );
      expect(matches.size).toBe(3);

      matches = fun(
        matches, ['node3', 'field1'],
        1, 'metric1', 'metric1', 'other', 1
      );
      expect(matches.size).toBe(3);
    });
  });

  describe('makeRegExp', () => {
    const fun = SearchUtils.makeRegExp;

    it('should make a regexp from any string', () => {
      expect(fun().source).toEqual((new RegExp()).source);
      expect(fun('que').source).toEqual((new RegExp('que')).source);
      // invalid string
      expect(fun('que[').source).toEqual((new RegExp('que\\[')).source);
    });
  });

  describe('matchPrefix', () => {
    const fun = SearchUtils.matchPrefix;

    it('returns true if the prefix matches the label', () => {
      expect(fun('label', 'prefix')).toBeFalsy();
      expect(fun('memory', 'mem')).toBeTruthy();
      expect(fun('mem', 'memory')).toBeFalsy();
      expect(fun('com.domain.label', 'label')).toBeTruthy();
      expect(fun('com.domain.Label', 'domainlabel')).toBeTruthy();
      expect(fun('com-Domain-label', 'domainlabel')).toBeTruthy();
      expect(fun('memory', 'mem.ry')).toBeTruthy();
    });
  });

  describe('parseQuery', () => {
    const fun = SearchUtils.parseQuery;

    it('should parse a metric value from a string', () => {
      expect(fun('')).toEqual(null);
      expect(fun('text')).toEqual({query: 'text'});
      expect(fun('prefix:text')).toEqual({prefix: 'prefix', query: 'text'});
      expect(fun(':text')).toEqual(null);
      expect(fun('text:')).toEqual(null);
      expect(fun('cpu > 1')).toEqual({comp: 'gt', metric: 'cpu', value: 1});
      expect(fun('cpu >')).toEqual(null);
    });
  });

  describe('parseValue', () => {
    const fun = SearchUtils.parseValue;

    it('should parse a metric value from a string', () => {
      expect(fun('1')).toEqual(1);
      expect(fun('1.34%')).toEqual(1.34);
      expect(fun('10kB')).toEqual(1024 * 10);
      expect(fun('1K')).toEqual(1024);
      expect(fun('2KB')).toEqual(2048);
      expect(fun('1MB')).toEqual(Math.pow(1024, 2));
      expect(fun('1m')).toEqual(Math.pow(1024, 2));
      expect(fun('1GB')).toEqual(Math.pow(1024, 3));
      expect(fun('1TB')).toEqual(Math.pow(1024, 4));
    });
  });

  describe('searchTopology', () => {
    const fun = SearchUtils.searchTopology;

    it('should return no matches on an empty topology', () => {
      const nodes = fromJS({});
      const matches = fun(nodes, {query: 'value'});
      expect(matches.size).toEqual(0);
    });

    it('should match on a node label', () => {
      const nodes = nodeSets.someNodes;
      let matches = fun(nodes, {query: 'node label 1'});
      expect(matches.size).toEqual(1);
      matches = fun(nodes, {query: 'node label'});
      expect(matches.size).toEqual(2);
    });

    it('should match on a metadata field', () => {
      const nodes = nodeSets.someNodes;
      const matches = fun(nodes, {query: 'value'});
      expect(matches.size).toEqual(2);
      expect(matches.getIn(['n1', 'metadata', 'fieldId1']).text).toEqual('value 1');
    });

    it('should match on a metric field', () => {
      const nodes = nodeSets.someNodes;
      const matches = fun(nodes, {comp: 'eq', metric: 'metric1', value: 1});
      expect(matches.size).toEqual(1);
      expect(matches.getIn(['n1', 'metrics', 'metric1']).metric).toBeTruthy();
    });

    it('should match on a property list value', () => {
      const nodes = nodeSets.someNodes;
      const matches = fun(nodes, {query: 'Value 1'});
      expect(matches.size).toEqual(2);
      expect(matches.getIn(['n2', 'property-lists']).size).toEqual(1);
      expect(matches.getIn(['n2', 'property-lists', 'label1']).text).toBe('Label Value 1');
    });

    it('should match on a generic table values', () => {
      const nodes = nodeSets.someNodes;
      const matches1 = fun(nodes, {query: 'xx'}).getIn(['n2', 'tables']);
      const matches2 = fun(nodes, {query: 'yy'}).getIn(['n2', 'tables']);
      const matches3 = fun(nodes, {query: 'zz'}).getIn(['n2', 'tables']);
      const matches4 = fun(nodes, {query: 'a'}).getIn(['n2', 'tables']);
      expect(matches1.size).toEqual(1);
      expect(matches2.size).toEqual(1);
      expect(matches3.size).toEqual(2);
      expect(matches4.size).toEqual(3);
      expect(matches1.get('row1_a').text).toBe('xxxa');
      expect(matches2.get('row2_a').text).toBe('yyyb');
      expect(matches3.get('row1_c').text).toBe('zzz1');
      expect(matches3.get('row2_c').text).toBe('zzz2');
      expect(matches4.get('row1_a').text).toBe('xxxa');
      expect(matches4.get('row3_a').text).toBe('Value 1');
      expect(matches4.get('row3_c').text).toBe('Value 3');
    });
  });
});
Example #20
Source File: topology-utils-test.js    From ThreatMapper with Apache License 2.0 5 votes vote down vote up
describe('TopologyUtils', () => {
  let TopologyUtils;
  let nodes;

  const nodeSets = {
    initial4: {
      edges: fromJS({
        'n1-n3': {id: 'n1-n3', source: 'n1', target: 'n3'},
        'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'},
        'n2-n4': {id: 'n2-n4', source: 'n2', target: 'n4'}
      }),
      nodes: fromJS({
        n1: {id: 'n1'},
        n2: {id: 'n2'},
        n3: {id: 'n3'},
        n4: {id: 'n4'}
      })
    },
    removeEdge24: {
      edges: fromJS({
        'n1-n3': {id: 'n1-n3', source: 'n1', target: 'n3'},
        'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'}
      }),
      nodes: fromJS({
        n1: {id: 'n1'},
        n2: {id: 'n2'},
        n3: {id: 'n3'},
        n4: {id: 'n4'}
      })
    },
    removeNode2: {
      edges: fromJS({
        'n1-n3': {id: 'n1-n3', source: 'n1', target: 'n3'},
        'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'}
      }),
      nodes: fromJS({
        n1: {id: 'n1'},
        n3: {id: 'n3'},
        n4: {id: 'n4'}
      })
    },
    removeNode23: {
      edges: fromJS({
        'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'}
      }),
      nodes: fromJS({
        n1: {id: 'n1'},
        n4: {id: 'n4'}
      })
    },
    single3: {
      edges: fromJS({}),
      nodes: fromJS({
        n1: {id: 'n1'},
        n2: {id: 'n2'},
        n3: {id: 'n3'}
      })
    },
    singlePortrait: {
      edges: fromJS({
        'n1-n4': {id: 'n1-n4', source: 'n1', target: 'n4'}
      }),
      nodes: fromJS({
        n1: {id: 'n1'},
        n2: {id: 'n2'},
        n3: {id: 'n3'},
        n4: {id: 'n4'},
        n5: {id: 'n5'}
      })
    }
  };

  beforeEach(() => {
    TopologyUtils = require('../topology-utils');
  });

  it('sets node degrees', () => {
    nodes = TopologyUtils.updateNodeDegrees(
      nodeSets.initial4.nodes,
      nodeSets.initial4.edges
    ).toJS();

    expect(nodes.n1.degree).toEqual(2);
    expect(nodes.n2.degree).toEqual(1);
    expect(nodes.n3.degree).toEqual(1);
    expect(nodes.n4.degree).toEqual(2);

    nodes = TopologyUtils.updateNodeDegrees(
      nodeSets.removeEdge24.nodes,
      nodeSets.removeEdge24.edges
    ).toJS();

    expect(nodes.n1.degree).toEqual(2);
    expect(nodes.n2.degree).toEqual(0);
    expect(nodes.n3.degree).toEqual(1);
    expect(nodes.n4.degree).toEqual(1);

    nodes = TopologyUtils.updateNodeDegrees(
      nodeSets.single3.nodes,
      nodeSets.single3.edges
    ).toJS();

    expect(nodes.n1.degree).toEqual(0);
    expect(nodes.n2.degree).toEqual(0);
    expect(nodes.n3.degree).toEqual(0);
  });

  describe('buildTopologyCacheId', () => {
    it('should generate a cache ID', () => {
      const fun = TopologyUtils.buildTopologyCacheId;
      expect(fun()).toEqual('');
      expect(fun('test')).toEqual('test');
      expect(fun(undefined, 'test')).toEqual('');
      expect(fun('test', {a: 1})).toEqual('test{"a":1}');
    });
  });

  describe('filterHiddenTopologies', () => {
    it('should filter out empty topos that set hide_if_empty=true', () => {
      const topos = [
        {hide_if_empty: true, id: 'a', stats: {filtered_nodes: 0, node_count: 0}},
        {hide_if_empty: true, id: 'b', stats: {filtered_nodes: 0, node_count: 1}},
        {hide_if_empty: true, id: 'c', stats: {filtered_nodes: 1, node_count: 0}},
        {hide_if_empty: false, id: 'd', stats: {filtered_nodes: 0, node_count: 0}}
      ];

      const res = TopologyUtils.filterHiddenTopologies(topos);
      expect(res.map(t => t.id)).toEqual(['b', 'c', 'd']);
    });
  });
});
Example #21
Source File: layout.js    From ThreatMapper with Apache License 2.0 5 votes vote down vote up
layersTopologyIdsSelector = createSelector(
  [
    state => state.get('currentTopologyId'),
  ],
  topologyId => fromJS(RESOURCE_VIEW_LAYERS[topologyId] || [])
)
Example #22
Source File: index.js    From strapi-plugin-config-sync with MIT License 5 votes vote down vote up
initialState = fromJS({
  configDiff: Map({}),
  partialDiff: List([]),
  isLoading: false,
  appEnv: 'development',
})
Example #23
Source File: node-filters.js    From ThreatMapper with Apache License 2.0 5 votes vote down vote up
loadedAllResourceLayersSelector = createSelector(
  [
    state => state.get('nodesByTopology').keySeq(),
  ],
  resourceViewLoadedTopologyIds => fromJS(RESOURCE_VIEW_LAYERS).keySeq()
    .every(topId => resourceViewLoadedTopologyIds.contains(topId))
)
Example #24
Source File: zoomable-canvas.js    From ThreatMapper with Apache License 2.0 5 votes vote down vote up
cacheZoom() {
    this.props.cacheZoomState(fromJS(this.cachableState()));
  }
Example #25
Source File: nodes-layout.js    From ThreatMapper with Apache License 2.0 5 votes vote down vote up
/**
 * Layout of nodes and edges
 * If a previous layout was given and not too much changed, the previous layout
 * is changed and returned. Otherwise does a new layout engine run.
 * @param  {Map} immNodes All nodes
 * @param  {Map} immEdges All edges
 * @param  {object} opts  width, height, margins, etc...
 * @return {object} graph object with nodes, edges, dimensions
 */
export function doLayout(immNodes, immEdges, opts) {
  const options = opts || {};
  const cacheId = buildTopologyCacheId(options.topologyId, options.topologyOptions);

  // one engine and node and edge caches per topology, to keep renderings similar
  if (options.noCache || !topologyCaches[cacheId]) {
    topologyCaches[cacheId] = {
      edgeCache: makeMap(),
      graph: new dagre.graphlib.Graph({}),
      nodeCache: makeMap()
    };
  }

  const cache = topologyCaches[cacheId];
  const cachedLayout = options.cachedLayout || cache.cachedLayout;
  const nodeCache = options.nodeCache || cache.nodeCache;
  const edgeCache = options.edgeCache || cache.edgeCache;
  const useCache = !options.forceRelayout && cachedLayout && nodeCache && edgeCache;
  const nodesWithDegrees = updateNodeDegrees(immNodes, immEdges);
  let layout;

  layoutRuns += 1;
  if (useCache && !hasUnseenNodes(immNodes, nodeCache)) {
    layoutRunsTrivial += 1;
    // trivial case: no new nodes have been added
    log('skip layout, trivial adjustment', layoutRunsTrivial, layoutRuns);
    layout = cloneLayout(cachedLayout, immNodes, immEdges);
    layout = copyLayoutProperties(layout, nodeCache, edgeCache);
  } else if (useCache
    && featureIsEnabledAny('layout-dance', 'layout-dance-single')
    && hasNewSingleNode(nodesWithDegrees, nodeCache)) {
    // special case: new nodes are 0-degree nodes, no need for layout run,
    // they will be laid out further below
    log('skip layout, only 0-degree node(s) added');
    layout = cloneLayout(cachedLayout, nodesWithDegrees, immEdges);
    layout = copyLayoutProperties(layout, nodeCache, edgeCache);
    layout = layoutSingleNodes(layout, opts);
  } else if (useCache
    && featureIsEnabledAny('layout-dance', 'layout-dance-rank')
    && hasNewNodesOfExistingRank(nodesWithDegrees, immEdges, nodeCache)) {
    // special case: few new nodes were added, no need for layout run,
    // they will inserted according to ranks
    log('skip layout, used rank-based insertion');
    layout = cloneLayout(cachedLayout, nodesWithDegrees, immEdges);
    layout = copyLayoutProperties(layout, nodeCache, edgeCache);
    layout = doLayoutNewNodesOfExistingRank(layout, nodeCache);
    layout = layoutSingleNodes(layout, opts);
  } else {
    // default case: the new layout is too different and refreshing is required
    layout = runLayoutEngine(cache.graph, nodesWithDegrees, immEdges, opts);
  }


  if (layout) {
    // Last line of defense - re-render everything if two nodes are too close to one another.
    if (minEuclideanDistanceBetweenPoints(layout.nodes) < NODE_CENTERS_SEPARATION_FACTOR) {
      layout = runLayoutEngine(cache.graph, nodesWithDegrees, immEdges, opts);
      trackAnalyticsEvent('scope.layout.graph.overlap');
    }

    // cache results
    cache.cachedLayout = layout;
    // only cache layout-related properties
    // NB: These properties must be immutable wrt a given node because properties of updated nodes
    // will be overwritten with the cached values, see copyLayoutProperties()
    cache.nodeCache = cache.nodeCache.merge(layout.nodes.map(n => fromJS(pick(n.toJS(), ['x', 'y', 'rank']))));
    cache.edgeCache = cache.edgeCache.merge(layout.edges);
  }

  return layout;
}
Example #26
Source File: nodes-chart-elements.js    From ThreatMapper with Apache License 2.0 5 votes vote down vote up
render() {
    const nodes = this.props.layoutNodes.toIndexedSeq()
      .map(this.nodeHighlightedDecorator)
      .map(this.nodeFocusedDecorator)
      .map(this.nodeBlurredDecorator)
      .map(this.nodeMatchesDecorator)
      .map(this.nodeNetworksDecorator)
      .map(this.nodeMetricDecorator)
      .map(this.nodeScaleDecorator)
      .groupBy(this.nodeDisplayLayer);

    const edges = this.props.layoutEdges.toIndexedSeq()
      .map(this.edgeHighlightedDecorator)
      .map(this.edgeFocusedDecorator)
      .map(this.edgeBlurredDecorator)
      .map(this.edgeScaleDecorator)
      .groupBy(this.edgeDisplayLayer);

    // NOTE: The elements need to be arranged into a single array outside
    // of DOM structure for React rendering engine to do smart rearrangements
    // without unnecessary re-rendering of the elements themselves. So e.g.
    // rendering the element layers individually below would be significantly slower.
    const orderedElements = makeList([
      edges.get(BLURRED_EDGES_LAYER, makeList()),
      nodes.get(BLURRED_NODES_LAYER, makeList()),
      fromJS([{ isActive: !!nodes.get(BLURRED_NODES_LAYER), isOverlay: true }]),
      edges.get(NORMAL_EDGES_LAYER, makeList()),
      nodes.get(NORMAL_NODES_LAYER, makeList()),
      edges.get(HIGHLIGHTED_EDGES_LAYER, makeList()),
      nodes.get(HIGHLIGHTED_NODES_LAYER, makeList()),
      edges.get(HOVERED_EDGES_LAYER, makeList()),
      nodes.get(HOVERED_NODES_LAYER, makeList()),
    ]).flatten(true);

    return (
      <g className="tour-step-anchor nodes-chart-elements">
        {orderedElements.map(this.renderElement)}
      </g>
    );
  }
Example #27
Source File: index.js    From strapi-plugin-sitemap with MIT License 5 votes vote down vote up
export default function sitemapReducer(state = initialState, action) {
  switch (action.type) {
    case GET_SETTINGS_SUCCEEDED:
      return state
        .update('settings', () => fromJS(action.settings))
        .updateIn(['settings', 'contentTypes'], () => fromJS(action.settings.get('contentTypes')))
        .updateIn(['settings', 'customEntries'], () => fromJS(action.settings.get('customEntries')))
        .update('initialData', () => fromJS(action.settings))
        .updateIn(['initialData', 'contentTypes'], () => fromJS(action.settings.get('contentTypes')))
        .updateIn(['initialData', 'customEntries'], () => fromJS(action.settings.get('customEntries')))
        .update('modifiedContentTypes', () => fromJS(action.settings.get('contentTypes')))
        .update('modifiedCustomEntries', () => fromJS(action.settings.get('customEntries')));
    case UPDATE_SETTINGS:
        return state
          .update('modifiedContentTypes', () => fromJS(action.settings.get('contentTypes')))
          .updateIn(['settings', 'contentTypes'], () => fromJS(action.settings.get('contentTypes')));
    case ON_CHANGE_CONTENT_TYPES:
      if (action.lang) {
        return state
          .updateIn(['modifiedContentTypes', action.contentType, 'languages', action.lang, action.key], () => action.value);
      } else {
        return state
          .updateIn(['modifiedContentTypes', action.contentType, action.key], () => action.value);
      }
    case ON_CHANGE_CUSTOM_ENTRY:
      return state
        .updateIn(['modifiedCustomEntries', action.url, action.key], () => action.value);
    case ON_CHANGE_SETTINGS:
        return state
          .updateIn(['settings', action.key], () => action.value);
    case DISCARD_ALL_CHANGES:
      return state
        .update('settings', () => state.get('initialData'))
        .update('modifiedContentTypes', () => state.getIn(['initialData', 'contentTypes']))
        .update('modifiedCustomEntries', () => state.getIn(['initialData', 'customEntries']));
    case DISCARD_MODIFIED_CONTENT_TYPES:
      return state
        .update('modifiedContentTypes', () => state.getIn(['settings', 'contentTypes']))
        .update('modifiedCustomEntries', () => state.getIn(['settings', 'customEntries']));
    case SUBMIT_MODAL:
      return state
        .updateIn(['settings', 'contentTypes'], () => state.get('modifiedContentTypes'))
        .updateIn(['settings', 'customEntries'], () => state.get('modifiedCustomEntries'));
    case DELETE_CONTENT_TYPE:
      if (state.getIn(['settings', 'contentTypes', action.key, 'languages']).size > 1) {
        return state
          .deleteIn(['settings', 'contentTypes', action.key, 'languages', action.lang])
          .deleteIn(['modifiedContentTypes', action.key, 'languages', action.lang]);
      } else {
        return state
          .deleteIn(['settings', 'contentTypes', action.key])
          .deleteIn(['modifiedContentTypes', action.key]);
      }
    case DELETE_CUSTOM_ENTRY:
      return state
        .deleteIn(['settings', 'customEntries', action.key])
        .deleteIn(['modifiedCustomEntries', action.key]);
    case GET_CONTENT_TYPES_SUCCEEDED:
      return state
        .update('contentTypes', () => action.contentTypes);
    case GET_LANGUAGES_SUCCEEDED:
      return state
        .update('languages', () => action.languages);
    case ON_SUBMIT_SUCCEEDED:
      return state
        .update('initialData', () => state.get('settings'));
    case GET_SITEMAP_INFO_SUCCEEDED:
      return state
        .update('info', () => fromJS(action.info));
    case GET_ALLOWED_FIELDS_SUCCEEDED:
      return state
        .update('allowedFields', () => action.fields);
    default:
      return state;
  }
}
Example #28
Source File: search-test.js    From ThreatMapper with Apache License 2.0 4 votes vote down vote up
describe('Search selectors', () => {
  const nodeSets = {
    someNodes: fromJS({
      n1: {
        id: 'n1',
        label: 'node label 1',
        metadata: [{
          id: 'fieldId1',
          label: 'Label 1',
          value: 'value 1'
        }],
        metrics: [{
          id: 'metric1',
          label: 'Metric 1',
          value: 1
        }]
      },
      n2: {
        id: 'n2',
        label: 'node label 2',
        metadata: [{
          id: 'fieldId2',
          label: 'Label 2',
          value: 'value 2'
        }],
        tables: [{
          id: 'metric1',
          rows: [{
            entries: {
              label: 'Label 1',
              value: 'Label Value 1'
            },
            id: 'label1'
          }, {
            entries: {
              label: 'Label 2',
              value: 'Label Value 2'
            },
            id: 'label2'
          }],
          type: 'property-list'
        }, {
          columns: [{
            id: 'a',
            label: 'A'
          }, {
            id: 'c',
            label: 'C'
          }],
          id: 'metric2',
          rows: [{
            entries: {
              a: 'xxxa',
              b: 'yyya',
              c: 'zzz1'
            },
            id: 'row1'
          }, {
            entries: {
              a: 'yyyb',
              b: 'xxxb',
              c: 'zzz2'
            },
            id: 'row2'
          }, {
            entries: {
              a: 'Value 1',
              b: 'Value 2',
              c: 'Value 3'
            },
            id: 'row3'
          }],
          type: 'multicolumn-table'
        }],
      },
    })
  };

  describe('searchNodeMatchesSelector', () => {
    const selector = SearchSelectors.searchNodeMatchesSelector;

    it('should return no matches on an empty topology', () => {
      const result = selector(fromJS({
        nodes: makeMap(),
        searchQuery: '',
      }));
      expect(result.filter(m => !m.isEmpty()).size).toEqual(0);
    });

    it('should return no matches when no query is present', () => {
      const result = selector(fromJS({
        nodes: nodeSets.someNodes,
        searchQuery: '',
      }));
      expect(result.filter(m => !m.isEmpty()).size).toEqual(0);
    });

    it('should return no matches when query matches nothing', () => {
      const result = selector(fromJS({
        nodes: nodeSets.someNodes,
        searchQuery: 'cantmatch',
      }));
      expect(result.filter(m => !m.isEmpty()).size).toEqual(0);
    });

    it('should return a matches when a query matches something', () => {
      const result = selector(fromJS({
        nodes: nodeSets.someNodes,
        searchQuery: 'value 2',
      }));
      expect(result.filter(m => !m.isEmpty()).size).toEqual(1);
    });
  });
});
Example #29
Source File: root.js    From ThreatMapper with Apache License 2.0 4 votes vote down vote up
export function rootReducer(state = initialState, action) {
  if (!action.type) {
    error('Payload missing a type!', action);
  }

  switch (action.type) {
    case ActionTypes.BLUR_SEARCH: {
      return state.set('searchFocused', false);
    }

    case ActionTypes.FOCUS_SEARCH: {
      return state.set('searchFocused', true);
    }

    case ActionTypes.CHANGE_TOPOLOGY_OPTION: {
      // set option on parent topology
      const topology = findTopologyById(state.get('topologies'), action.topologyId);
      if (topology) {
        const topologyId = topology.get('parentId') || topology.get('id');
        const optionKey = ['topologyOptions', topologyId, action.option];
        const currentOption = state.getIn(optionKey);

        if (!isEqual(currentOption, action.value)) {
          state = clearNodes(state);
        }

        state = state.setIn(optionKey, action.value);
      }
      return state;
    }

    case ActionTypes.SET_VIEWPORT_DIMENSIONS: {
      return state.mergeIn(['viewport'], {
        height: action.height,
        width: action.width,
      });
    }

    case ActionTypes.SET_EXPORTING_GRAPH: {
      return state.set('exportingGraph', action.exporting);
    }

    case ActionTypes.SORT_ORDER_CHANGED: {
      return state.merge({
        gridSortedBy: action.sortedBy,
        gridSortedDesc: action.sortedDesc,
      });
    }

    case ActionTypes.SET_VIEW_MODE: {
      return state.set('topologyViewMode', action.viewMode);
    }

    case ActionTypes.CACHE_ZOOM_STATE: {
      return state.setIn(activeTopologyZoomCacheKeyPathSelector(state), action.zoomState);
    }

    case ActionTypes.CLEAR_CONTROL_ERROR: {
      return state.removeIn(['controlStatus', action.nodeId, 'error']);
    }

    case ActionTypes.CLICK_BACKGROUND: {
      if (state.get('showingHelp')) {
        state = state.set('showingHelp', false);
      }

      if (state.get('showingTroubleshootingMenu')) {
        state = state.set('showingTroubleshootingMenu', false);
      }
      return closeAllNodeDetails(state);
    }

    case ActionTypes.CLICK_CLOSE_DETAILS: {
      return closeNodeDetails(state, action.nodeId);
    }

    case ActionTypes.CLOSE_TERMINAL: {
      return state.update('controlPipes', controlPipes => controlPipes.clear());
    }

    case ActionTypes.CLICK_FORCE_RELAYOUT: {
      return state.set('forceRelayout', action.forceRelayout);
    }

    case ActionTypes.CLICK_NODE: {
      const prevSelectedNodeId = state.get('selectedNodeId');
      const prevDetailsStackSize = state.get('nodeDetails').size;

      // click on sibling closes all
      state = closeAllNodeDetails(state);

      // select new node if it's not the same (in that case just delesect)
      if (prevDetailsStackSize > 1 || prevSelectedNodeId !== action.nodeId) {
        // dont set origin if a node was already selected, suppresses animation
        const origin = prevSelectedNodeId === null ? action.origin : null;
        state = state.setIn(
          ['nodeDetails', action.nodeId],
          {
            id: action.nodeId,
            label: action.label,
            origin,
            topologyId: action.topologyId || state.get('currentTopologyId'),
          }
        );
        state = state.set('selectedNodeId', action.nodeId);
      }
      return state;
    }

    case ActionTypes.CLICK_RELATIVE: {
      if (state.hasIn(['nodeDetails', action.nodeId])) {
        // bring to front
        const details = state.getIn(['nodeDetails', action.nodeId]);
        state = state.deleteIn(['nodeDetails', action.nodeId]);
        state = state.setIn(['nodeDetails', action.nodeId], details);
      } else {
        state = state.setIn(
          ['nodeDetails', action.nodeId],
          {
            id: action.nodeId,
            label: action.label,
            origin: action.origin,
            topologyId: action.topologyId
          }
        );
      }
      return state;
    }

    case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE: {
      state = state.update(
        'nodeDetails',
        nodeDetails => nodeDetails.filter((v, k) => k === action.nodeId)
      );
      state = state.update('controlPipes', controlPipes => controlPipes.clear());
      state = state.set('selectedNodeId', action.nodeId);

      if (action.topologyId !== state.get('currentTopologyId')) {
        state = setTopology(state, action.topologyId);
        state = clearNodes(state);
      }

      return state;
    }

    case ActionTypes.CLICK_TOPOLOGY: {
      state = closeAllNodeDetails(state);

      const currentTopologyId = state.get('currentTopologyId');
      if (action.topologyId !== currentTopologyId) {
        state = setTopology(state, action.topologyId);
        state = clearNodes(state);
      }

      return state;
    }

    //
    // time control
    //

    case ActionTypes.RESUME_TIME: {
      state = state.set('timeTravelTransitioning', true);
      return state.set('pausedAt', null);
    }

    case ActionTypes.PAUSE_TIME_AT_NOW: {
      state = state.set('timeTravelTransitioning', false);
      return state.set('pausedAt', moment().utc().format());
    }

    case ActionTypes.JUMP_TO_TIME: {
      state = state.set('timeTravelTransitioning', true);
      return state.set('pausedAt', action.timestamp);
    }

    case ActionTypes.FINISH_TIME_TRAVEL_TRANSITION: {
      state = state.set('timeTravelTransitioning', false);
      return clearNodes(state);
    }

    //
    // websockets
    //

    case ActionTypes.OPEN_WEBSOCKET: {
      return state.set('websocketClosed', false);
    }

    case ActionTypes.CLOSE_WEBSOCKET: {
      return state.set('websocketClosed', true);
    }

    //
    // networks
    //

    case ActionTypes.SHOW_NETWORKS: {
      if (!action.visible) {
        state = state.set('selectedNetwork', null);
        state = state.set('pinnedNetwork', null);
      }
      return state.set('showingNetworks', action.visible);
    }

    case ActionTypes.SELECT_NETWORK: {
      return state.set('selectedNetwork', action.networkId);
    }

    case ActionTypes.PIN_NETWORK: {
      return state.merge({
        pinnedNetwork: action.networkId,
        selectedNetwork: action.networkId
      });
    }

    case ActionTypes.UNPIN_NETWORK: {
      return state.merge({
        pinnedNetwork: null,
      });
    }

    //
    // metrics
    //

    case ActionTypes.HOVER_METRIC: {
      return state.set('hoveredMetricType', action.metricType);
    }

    case ActionTypes.UNHOVER_METRIC: {
      return state.set('hoveredMetricType', null);
    }

    case ActionTypes.PIN_METRIC: {
      return state.set('pinnedMetricType', action.metricType);
    }

    case ActionTypes.UNPIN_METRIC: {
      return state.set('pinnedMetricType', null);
    }

    case ActionTypes.SHOW_HELP: {
      return state.set('showingHelp', true);
    }

    case ActionTypes.HIDE_HELP: {
      return state.set('showingHelp', false);
    }

    case ActionTypes.DESELECT_NODE: {
      return closeNodeDetails(state);
    }

    case ActionTypes.DO_CONTROL: {
      return state.setIn(['controlStatus', action.nodeId], makeMap({
        error: null,
        pending: true
      }));
    }

    case ActionTypes.ENTER_EDGE: {
      return state.set('mouseOverEdgeId', action.edgeId);
    }

    case ActionTypes.ENTER_NODE: {
      return state.set('mouseOverNodeId', action.nodeId);
    }

    case ActionTypes.LEAVE_EDGE: {
      return state.set('mouseOverEdgeId', null);
    }

    case ActionTypes.LEAVE_NODE: {
      return state.set('mouseOverNodeId', null);
    }

    case ActionTypes.DO_CONTROL_ERROR: {
      return state.setIn(['controlStatus', action.nodeId], makeMap({
        error: action.error,
        pending: false
      }));
    }
    case ActionTypes.DO_CONTROL_SUCCESS: {
      return state.setIn(['controlStatus', action.nodeId], makeMap({
        error: null,
        pending: false
      }));
    }

    case ActionTypes.UPDATE_SEARCH: {
      state = state.set('pinnedSearches', makeList(action.pinnedSearches));
      state = state.set('searchQuery', action.searchQuery || '');
      return applyPinnedSearches(state);
    }

    case ActionTypes.RECEIVE_CONTROL_NODE_REMOVED: {
      return closeNodeDetails(state, action.nodeId);
    }

    case ActionTypes.RECEIVE_CONTROL_PIPE: {
      return state.setIn(['controlPipes', action.pipeId], makeOrderedMap({
        control: action.control,
        id: action.pipeId,
        nodeId: action.nodeId,
        raw: action.rawTty,
        resizeTtyControl: action.resizeTtyControl
      }));
    }

    case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS: {
      if (state.hasIn(['controlPipes', action.pipeId])) {
        state = state.setIn(['controlPipes', action.pipeId, 'status'], action.status);
      }
      return state;
    }

    case ActionTypes.RECEIVE_ERROR: {
      if (state.get('errorUrl') !== null) {
        state = state.set('errorUrl', action.errorUrl);
      }
      return state;
    }

    case ActionTypes.RECEIVE_NODE_DETAILS: {
      // Ignore the update if paused and the timestamp didn't change.
      const setTimestamp = state.getIn(['nodeDetails', action.details.id, 'timestamp']);
      if (isPausedSelector(state) && action.requestTimestamp === setTimestamp) {
        return state;
      }

      state = state.set('errorUrl', null);

      // disregard if node is not selected anymore
      if (state.hasIn(['nodeDetails', action.details.id])) {
        state = state.updateIn(['nodeDetails', action.details.id], obj => ({
          ...obj,
          details: action.details,
          notFound: false,
          timestamp: action.requestTimestamp,
        }));
      }
      return state;
    }

    case ActionTypes.SET_RECEIVED_NODES_DELTA: {
      // Turn on the table view if the graph is too complex, but skip
      // this block if the user has already loaded topologies once.
      if (!state.get('initialNodesLoaded') && !state.get('nodesLoaded')) {
        if (state.get('topologyViewMode') === GRAPH_VIEW_MODE) {
          state = graphExceedsComplexityThreshSelector(state)
            ? state.set('topologyViewMode', TABLE_VIEW_MODE) : state;
        }
        state = state.set('initialNodesLoaded', true);
      }
      return state.set('nodesLoaded', true);
    }

    case ActionTypes.RECEIVE_NODES_DELTA: {
      // Ignore periodic nodes updates after the first load when paused.
      if (state.get('nodesLoaded') && state.get('pausedAt')) {
        return state;
      }

      log(
        'RECEIVE_NODES_DELTA',
        'remove', size(action.delta.remove),
        'update', size(action.delta.update),
        'add', size(action.delta.add),
        'reset', action.delta.reset
      );

      if (action.delta.reset) {
        state = state.set('nodes', makeMap());
      }

      // remove nodes that no longer exist
      each(action.delta.remove, (nodeId) => {
        state = state.deleteIn(['nodes', nodeId]);
      });

      // update existing nodes
      each(action.delta.update, (node) => {
        if (state.hasIn(['nodes', node.id])) {
          // TODO: Implement a manual deep update here, as it might bring a great benefit
          // to our nodes selectors (e.g. layout engine would be completely bypassed if the
          // adjacencies would stay the same but the metrics would get updated).
          state = state.setIn(['nodes', node.id], fromJS(node));
        }
      });

      // add new nodes
      each(action.delta.add, (node) => {
        state = state.setIn(['nodes', node.id], fromJS(node));
      });

      return updateStateFromNodes(state);
    }

    case ActionTypes.RECEIVE_NODES: {
      state = state.set('timeTravelTransitioning', false);
      state = state.set('nodes', fromJS(action.nodes));
      state = state.set('nodesLoaded', true);
      return updateStateFromNodes(state);
    }

    case ActionTypes.RECEIVE_NODES_FOR_TOPOLOGY: {
      return state.setIn(['nodesByTopology', action.topologyId], fromJS(action.nodes));
    }

    case ActionTypes.RECEIVE_NOT_FOUND: {
      if (state.hasIn(['nodeDetails', action.nodeId])) {
        state = state.updateIn(['nodeDetails', action.nodeId], obj => ({
          ...obj,
          notFound: true,
          timestamp: action.requestTimestamp,
        }));
      }
      return state;
    }

    case ActionTypes.RECEIVE_TOPOLOGIES: {
      state = state.set('errorUrl', null);
      state = state.update('topologyUrlsById', topologyUrlsById => topologyUrlsById.clear());
      state = processTopologies(state, action.topologies);
      const currentTopologyId = state.get('currentTopologyId');
      if (!currentTopologyId || !findTopologyById(state.get('topologies'), currentTopologyId)) {
        state = state.set('currentTopologyId', getDefaultTopology(state.get('topologies')));
        log(`Set currentTopologyId to ${state.get('currentTopologyId')}`);
      }
      state = setTopology(state, state.get('currentTopologyId'));

      // Expand topology options with topologies' defaults on first load, but let
      // the current state of topologyOptions (which at this point reflects the
      // URL state) still take the precedence over defaults.
      if (!state.get('topologiesLoaded')) {
        const options = getDefaultTopologyOptions(state).mergeDeep(state.get('topologyOptions'));
        state = state.set('topologyOptions', options);
        state = state.set('topologiesLoaded', true);
      }

      return state;
    }

    case ActionTypes.RECEIVE_API_DETAILS: {
      state = state.set('errorUrl', null);

      return state.merge({
        capabilities: action.capabilities,
        hostname: action.hostname,
        plugins: action.plugins,
        version: action.version,
        versionUpdate: action.newVersion,
      });
    }

    case ActionTypes.ROUTE_TOPOLOGY: {
      state = state.set('routeSet', true);
      state = state.set('pinnedSearches', makeList(action.state.pinnedSearches));
      state = state.set('searchQuery', action.state.searchQuery || '');
      if (state.get('currentTopologyId') !== action.state.topologyId) {
        state = clearNodes(state);
      }
      state = setTopology(state, action.state.topologyId);
      state = state.merge({
        pinnedMetricType: action.state.pinnedMetricType,
        selectedNodeId: action.state.selectedNodeId,
      });
      if (action.state.topologyOptions) {
        const options = getDefaultTopologyOptions(state).mergeDeep(action.state.topologyOptions);
        state = state.set('topologyOptions', options);
      }
      if (action.state.topologyViewMode) {
        state = state.set('topologyViewMode', action.state.topologyViewMode);
      }
      if (action.state.gridSortedBy) {
        state = state.set('gridSortedBy', action.state.gridSortedBy);
      }
      if (action.state.gridSortedDesc !== undefined) {
        state = state.set('gridSortedDesc', action.state.gridSortedDesc);
      }
      if (action.state.contrastMode !== undefined) {
        state = state.set('contrastMode', action.state.contrastMode);
      }
      if (action.state.showingNetworks) {
        state = state.set('showingNetworks', action.state.showingNetworks);
      }
      if (action.state.pinnedNetwork) {
        state = state.set('pinnedNetwork', action.state.pinnedNetwork);
        state = state.set('selectedNetwork', action.state.pinnedNetwork);
      }
      if (action.state.controlPipe) {
        state = state.set('controlPipes', makeOrderedMap({
          [action.state.controlPipe.id]:
            makeOrderedMap(action.state.controlPipe)
        }));
      } else {
        state = state.update('controlPipes', controlPipes => controlPipes.clear());
      }
      if (action.state.nodeDetails) {
        const actionNodeDetails = makeOrderedMap(action.state.nodeDetails.map(h => [h.id, h]));
        // check if detail IDs have changed
        if (!isDeepEqual(state.get('nodeDetails').keySeq(), actionNodeDetails.keySeq())) {
          state = state.set('nodeDetails', actionNodeDetails);
        }
      } else {
        state = state.update('nodeDetails', nodeDetails => nodeDetails.clear());
      }
      return state;
    }

    case ActionTypes.DEBUG_TOOLBAR_INTERFERING: {
      return action.fn(state);
    }

    case ActionTypes.TOGGLE_TROUBLESHOOTING_MENU: {
      return state.set('showingTroubleshootingMenu', !state.get('showingTroubleshootingMenu'));
    }

    case ActionTypes.CHANGE_INSTANCE: {
      state = closeAllNodeDetails(state);
      return state;
    }

    case ActionTypes.TOGGLE_CONTRAST_MODE: {
      return state.set('contrastMode', action.enabled);
    }

    case ActionTypes.SHUTDOWN: {
      return clearNodes(state);
    }

    case ActionTypes.MONITOR_STATE: {
      return state.set('monitor', action.monitor);
    }

    case ActionTypes.SET_STORE_VIEW_STATE: {
      return state.set('storeViewState', action.storeViewState);
    }

    default: {
      return state;
    }
  }
}