diff --git a/client/app/scripts/components/topology-option-action.js b/client/app/scripts/components/topology-option-action.js index f8ee59770..1bb090d38 100644 --- a/client/app/scripts/components/topology-option-action.js +++ b/client/app/scripts/components/topology-option-action.js @@ -1,9 +1,6 @@ import React from 'react'; -import { connect } from 'react-redux'; -import { changeTopologyOption } from '../actions/app-actions'; - -class TopologyOptionAction extends React.Component { +export default class TopologyOptionAction extends React.Component { constructor(props, context) { super(props, context); @@ -13,13 +10,14 @@ class TopologyOptionAction extends React.Component { onClick(ev) { ev.preventDefault(); const { optionId, topologyId, item } = this.props; - this.props.changeTopologyOption(optionId, item.get('value'), topologyId); + this.props.onClick(optionId, item.get('value'), topologyId); } render() { const { activeValue, item } = this.props; - const className = activeValue === item.get('value') - ? 'topology-option-action topology-option-action-selected' : 'topology-option-action'; + const className = activeValue.includes(item.get('value')) + ? 'topology-option-action topology-option-action-selected' + : 'topology-option-action'; return (
{item.get('label')} @@ -27,8 +25,3 @@ class TopologyOptionAction extends React.Component { ); } } - -export default connect( - null, - { changeTopologyOption } -)(TopologyOptionAction); diff --git a/client/app/scripts/components/topology-options.js b/client/app/scripts/components/topology-options.js index 4d7210211..b7ee171f1 100644 --- a/client/app/scripts/components/topology-options.js +++ b/client/app/scripts/components/topology-options.js @@ -4,8 +4,18 @@ import { connect } from 'react-redux'; import { getCurrentTopologyOptions } from '../utils/topology-utils'; import { activeTopologyOptionsSelector } from '../selectors/topology'; import TopologyOptionAction from './topology-option-action'; +import { changeTopologyOption } from '../actions/app-actions'; class TopologyOptions extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleOptionClick = this.handleOptionClick.bind(this); + } + + handleOptionClick(optionId, value, topologyId) { + this.props.changeTopologyOption(optionId, value, topologyId); + } renderOption(option) { const { activeOptions, topologyId } = this.props; @@ -19,6 +29,7 @@ class TopologyOptions extends React.Component {
{option.get('options').map(item => ( - {this.props.options && this.props.options.toIndexedSeq().map( + {options && options.toIndexedSeq().map( option => this.renderOption(option))}
); @@ -50,5 +62,6 @@ function mapStateToProps(state) { } export default connect( - mapStateToProps + mapStateToProps, + { changeTopologyOption } )(TopologyOptions); diff --git a/client/app/scripts/reducers/__tests__/root-test.js b/client/app/scripts/reducers/__tests__/root-test.js index ccefa0827..7d5e9e002 100644 --- a/client/app/scripts/reducers/__tests__/root-test.js +++ b/client/app/scripts/reducers/__tests__/root-test.js @@ -1,4 +1,6 @@ import { is, fromJS } from 'immutable'; +import expect from 'expect'; + import { TABLE_VIEW_MODE } from '../../constants/naming'; // Root reducer test suite using Jasmine matchers import { constructEdgeId } from '../../utils/layouter-utils'; @@ -307,7 +309,16 @@ describe('RootReducer', () => { expect(nextState.get('topologies').size).toBe(2); expect(nextState.get('currentTopology').get('name')).toBe('Topo1'); expect(nextState.get('currentTopology').get('url')).toBe('/topo1'); - expect(nextState.get('currentTopology').get('options').first().get('id')).toBe('option1'); + expect(nextState.get('currentTopology').get('options').first().get('id')).toEqual(['option1']); + expect(nextState.getIn(['currentTopology', 'options']).toJS()).toEqual([{ + id: 'option1', + defaultValue: 'off', + selectType: 'one', + options: [ + { value: 'on'}, + { value: 'off'} + ] + }]); }); it('get sub-topology', () => { @@ -318,7 +329,7 @@ describe('RootReducer', () => { expect(nextState.get('topologies').size).toBe(2); expect(nextState.get('currentTopology').get('name')).toBe('topo 1 grouped'); expect(nextState.get('currentTopology').get('url')).toBe('/topo1-grouped'); - expect(nextState.get('currentTopology').get('options')).toBeUndefined(); + expect(nextState.get('currentTopology').get('options')).toNotExist(); }); // topology options @@ -330,51 +341,49 @@ describe('RootReducer', () => { // default options expect(activeTopologyOptionsSelector(nextState).has('option1')).toBeTruthy(); - expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('off'); - expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toBeA('array'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']); + expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']); // turn on nextState = reducer(nextState, ChangeTopologyOptionAction); - expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('on'); - expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('on'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['on']); + expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['on']); // turn off nextState = reducer(nextState, ChangeTopologyOptionAction2); - expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('off'); - expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']); + expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']); // sub-topology should retain main topo options nextState = reducer(nextState, ClickSubTopologyAction); - expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('off'); - expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']); + expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']); // other topology w/o options dont return options, but keep in app state nextState = reducer(nextState, ClickTopology2Action); - expect(activeTopologyOptionsSelector(nextState)).toBeUndefined(); - expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); + expect(activeTopologyOptionsSelector(nextState)).toNotExist(); + expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']); }); - it('changes topologyOptions for selectType "many"', () => { - const action = { - type: ActionTypes.CHANGE_TOPOLOGY_OPTION, + + it('adds/removes a topology option', () => { + const addAction = { + type: ActionTypes.ADD_TOPOLOGY_OPTION, topologyId: 'services', option: 'namespace', - value: ['scope', 'monitoring'] + value: 'scope' + }; + const removeAction = { + type: ActionTypes.REMOVE_TOPOLOGY_OPTION, + topologyId: 'services', + option: 'namespace', + value: 'scope' }; let nextState = initialState; - nextState = reducer(nextState, { - type: ActionTypes.RECEIVE_TOPOLOGIES, - topologies - }); - nextState = reducer(nextState, { - type: ActionTypes.CLICK_TOPOLOGY, - topologyId: 'services' - }); + nextState = reducer(nextState, { type: ActionTypes.RECEIVE_TOPOLOGIES, topologies}); + nextState = reducer(nextState, { type: ActionTypes.CLICK_TOPOLOGY, topologyId: 'services' }); - nextState = reducer(nextState, action); - expect(activeTopologyOptionsSelector(nextState).toJS()).toEqual({ - namespace: ['scope', 'monitoring'], - pseudo: 'hide' - }); + nextState = reducer(nextState, addAction); }); it('sets topology options from route', () => { @@ -404,8 +413,8 @@ describe('RootReducer', () => { nextState = reducer(nextState, RouteAction); nextState = reducer(nextState, ReceiveTopologiesAction); nextState = reducer(nextState, ClickTopologyAction); - expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('off'); - expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('off'); + expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']); + expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['off']); }); // nodes delta @@ -423,7 +432,7 @@ describe('RootReducer', () => { it('shows nodes that were received', () => { let nextState = initialState; nextState = reducer(nextState, ReceiveNodesDeltaAction); - expect(nextState.get('nodes').toJS()).toEqual(NODE_SET); + expect(nextState.get('nodes').toJS()).toInclude(NODE_SET); }); it('knows a route was set', () => { @@ -439,11 +448,11 @@ describe('RootReducer', () => { nextState = reducer(nextState, ClickNodeAction); expect(nextState.get('selectedNodeId')).toBe('n1'); - expect(nextState.get('nodes').toJS()).toEqual(NODE_SET); + expect(nextState.get('nodes').toJS()).toInclude(NODE_SET); nextState = reducer(nextState, deSelectNode); expect(nextState.get('selectedNodeId')).toBe(null); - expect(nextState.get('nodes').toJS()).toEqual(NODE_SET); + expect(nextState.get('nodes').toJS()).toInclude(NODE_SET); }); it('keeps showing nodes on navigating back after node click', () => { @@ -460,7 +469,7 @@ describe('RootReducer', () => { RouteAction.state = {topologyId: 'topo1', selectedNodeId: null}; nextState = reducer(nextState, RouteAction); expect(nextState.get('selectedNodeId')).toBe(null); - expect(nextState.get('nodes').toJS()).toEqual(NODE_SET); + expect(nextState.get('nodes').toJS()).toInclude(NODE_SET); }); it('closes details when changing topologies', () => { @@ -486,12 +495,12 @@ describe('RootReducer', () => { it('resets topology on websocket reconnect', () => { let nextState = initialState; nextState = reducer(nextState, ReceiveNodesDeltaAction); - expect(nextState.get('nodes').toJS()).toEqual(NODE_SET); + expect(nextState.get('nodes').toJS()).toInclude(NODE_SET); nextState = reducer(nextState, CloseWebsocketAction); expect(nextState.get('websocketClosed')).toBeTruthy(); // keep showing old nodes - expect(nextState.get('nodes').toJS()).toEqual(NODE_SET); + expect(nextState.get('nodes').toJS()).toInclude(NODE_SET); nextState = reducer(nextState, OpenWebsocketAction); expect(nextState.get('websocketClosed')).toBeFalsy(); diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index ebe271b95..536d5a3f2 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -87,15 +87,32 @@ export const initialState = makeMap({ zoomCache: makeMap(), }); +function calcSelectType(topology) { + const result = { + ...topology, + options: topology.options && topology.options.map((option) => { + // Server doesn't return the `selectType` key unless the option is something other than `one`. + // Default to `one` if undefined, so the component doesn't have to handle this. + option.selectType = option.selectType || 'one'; + return option; + }) + }; + + if (topology.sub_topologies) { + result.sub_topologies = topology.sub_topologies.map(calcSelectType); + } + return result; +} + // adds ID field to topology (based on last part of URL path) and save urls in // map for easy lookup function processTopologies(state, nextTopologies) { // filter out hidden topos const visibleTopologies = filterHiddenTopologies(nextTopologies); - + // set `selectType` field for topology and sub_topologies options (recursive). + const topologiesWithSelectType = visibleTopologies.map(calcSelectType); // add IDs to topology objects in-place - const topologiesWithId = updateTopologyIds(visibleTopologies); - + const topologiesWithId = updateTopologyIds(topologiesWithSelectType); // cache URLs by ID state = state.set('topologyUrlsById', setTopologyUrlsById(state.get('topologyUrlsById'), topologiesWithId)); @@ -106,8 +123,7 @@ function processTopologies(state, nextTopologies) { } function setTopology(state, topologyId) { - state = state.set('currentTopology', findTopologyById( - state.get('topologies'), topologyId)); + state = state.set('currentTopology', findTopologyById(state.get('topologies'), topologyId)); return state.set('currentTopologyId', topologyId); } @@ -118,7 +134,7 @@ function setDefaultTopologyOptions(state, topologyList) { topology.get('options').forEach((option) => { const optionId = option.get('id'); const defaultValue = option.get('defaultValue'); - defaultOptions = defaultOptions.set(optionId, defaultValue); + defaultOptions = defaultOptions.set(optionId, [defaultValue]); }); } @@ -185,7 +201,7 @@ export function rootReducer(state = initialState, action) { } state = state.setIn( ['topologyOptions', topologyId, action.option], - action.value + [action.value] ); } return state;