diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 6eebefec09..8e70a45a0f 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -410,7 +410,8 @@ export default class NodesChart extends React.Component { scale: nodeScale, margins: MARGINS, forceRelayout: props.forceRelayout, - topologyId: this.props.topologyId + topologyId: this.props.topologyId, + topologyOptions: this.props.topologyOptions }; const timedLayouter = timely(doLayout); diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index 97b13fe4e8..9707f1c668 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -26,6 +26,17 @@ function fromGraphNodeId(encodedId) { return encodedId.replace('', '.'); } +function buildCacheIdFromOptions(options) { + if (options) { + let id = options.topologyId; + if (options.topologyOptions) { + id += JSON.stringify(options.topologyOptions); + } + return id; + } + return ''; +} + /** * Layout engine runner * After the layout engine run nodes and edges have x-y-coordinates. Engine is @@ -343,18 +354,18 @@ function copyLayoutProperties(layout, nodeCache, edgeCache) { */ export function doLayout(immNodes, immEdges, opts) { const options = opts || {}; - const topologyId = options.topologyId || 'noId'; + const cacheId = buildCacheIdFromOptions(options); // one engine and node and edge caches per topology, to keep renderings similar - if (!topologyCaches[topologyId]) { - topologyCaches[topologyId] = { + if (!topologyCaches[cacheId]) { + topologyCaches[cacheId] = { nodeCache: makeMap(), edgeCache: makeMap(), graph: new dagre.graphlib.Graph({}) }; } - const cache = topologyCaches[topologyId]; + const cache = topologyCaches[cacheId]; const cachedLayout = options.cachedLayout || cache.cachedLayout; const nodeCache = options.nodeCache || cache.nodeCache; const edgeCache = options.edgeCache || cache.edgeCache; diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index cf54673a61..d2e37fa582 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -107,6 +107,7 @@ export default class App extends React.Component { highlightedEdgeIds={this.state.highlightedEdgeIds} detailsWidth={detailsWidth} selectedNodeId={this.state.selectedNodeId} topMargin={topMargin} forceRelayout={this.state.forceRelayout} + topologyOptions={this.state.activeTopologyOptions} topologyId={this.state.currentTopologyId} /> diff --git a/client/app/scripts/components/topology-option-action.js b/client/app/scripts/components/topology-option-action.js index 665bbe2511..65196d5c5d 100644 --- a/client/app/scripts/components/topology-option-action.js +++ b/client/app/scripts/components/topology-option-action.js @@ -3,6 +3,7 @@ import React from 'react'; import { changeTopologyOption } from '../actions/app-actions'; export default class TopologyOptionAction extends React.Component { + constructor(props, context) { super(props, context); this.onClick = this.onClick.bind(this); @@ -10,14 +11,18 @@ export default class TopologyOptionAction extends React.Component { onClick(ev) { ev.preventDefault(); - changeTopologyOption(this.props.option, this.props.value, this.props.topologyId); + const { optionId, topologyId, item } = this.props; + changeTopologyOption(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'; return ( - - {this.props.value} - +
+ {item.get('label')} +
); } } diff --git a/client/app/scripts/components/topology-options.js b/client/app/scripts/components/topology-options.js index af67b61866..31f02e5832 100644 --- a/client/app/scripts/components/topology-options.js +++ b/client/app/scripts/components/topology-options.js @@ -3,66 +3,28 @@ import React from 'react'; import TopologyOptionAction from './topology-option-action'; export default class TopologyOptions extends React.Component { - renderAction(action, option, topologyId) { - return ( - - ); - } - /** - * transforms a list of options into one sidebar-item. - * The sidebar text comes from the active option. the actions come from the - * remaining items. - */ - renderOption(items) { - let activeText; - let activeValue; - const actions = []; - const activeOptions = this.props.activeOptions; - const topologyId = this.props.topologyId; - const option = items.first().get('option'); - - // find active option value - if (activeOptions && activeOptions.has(option)) { - activeValue = activeOptions.get(option); - } else { - // get default value - items.forEach(item => { - if (item.get('default')) { - activeValue = item.get('value'); - } - }); - } - - // render active option as text, add other options as actions - items.forEach(item => { - if (item.get('value') === activeValue) { - activeText = item.get('display'); - } else { - actions.push(this.renderAction(item.get('value'), item.get('option'), topologyId)); - } - }, this); + renderOption(option) { + const { activeOptions, topologyId } = this.props; + const optionId = option.get('id'); + const activeValue = activeOptions && activeOptions.has(optionId) + ? activeOptions.get(optionId) : option.get('defaultValue'); return ( -
- {activeText} - - {actions} - +
+
+ {option.get('options').map(item => )} +
); } render() { - 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 (
- {options.toIndexedSeq().map(items => this.renderOption(items))} + {this.props.options.toIndexedSeq().map(option => this.renderOption(option))}
); } diff --git a/client/app/scripts/stores/__tests__/app-store-test.js b/client/app/scripts/stores/__tests__/app-store-test.js index 85065a7f80..5ccee481b9 100644 --- a/client/app/scripts/stores/__tests__/app-store-test.js +++ b/client/app/scripts/stores/__tests__/app-store-test.js @@ -122,12 +122,14 @@ describe('AppStore', () => { topologies: [{ url: '/topo1', name: 'Topo1', - options: { - option1: [ + options: [{ + id: 'option1', + defaultValue: 'off', + options: [ {value: 'on'}, - {value: 'off', default: true} + {value: 'off'} ] - }, + }], stats: { node_count: 1 }, @@ -165,13 +167,13 @@ describe('AppStore', () => { }); it('get current topology', () => { - registeredCallback(ClickTopologyAction); registeredCallback(ReceiveTopologiesAction); + registeredCallback(ClickTopologyAction); expect(AppStore.getTopologies().size).toBe(2); expect(AppStore.getCurrentTopology().get('name')).toBe('Topo1'); expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1'); - expect(AppStore.getCurrentTopologyOptions().get('option1')).toBeDefined(); + expect(AppStore.getCurrentTopologyOptions().first().get('id')).toBe('option1'); }); it('get sub-topology', () => { diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index cd79d84d2c..66abc2c2a6 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -84,13 +84,11 @@ function setTopology(topologyId) { function setDefaultTopologyOptions(topologyList) { topologyList.forEach(topology => { let defaultOptions = makeOrderedMap(); - if (topology.has('options')) { - topology.get('options').forEach((items, option) => { - items.forEach(item => { - if (item.get('default') === true) { - defaultOptions = defaultOptions.set(option, item.get('value')); - } - }); + if (topology.has('options') && topology.get('options')) { + topology.get('options').forEach((option) => { + const optionId = option.get('id'); + const defaultValue = option.get('defaultValue'); + defaultOptions = defaultOptions.set(optionId, defaultValue); }); } diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 27198b429e..ff19b85621 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -36,6 +36,7 @@ @details-window-width: 420px; @details-window-padding-left: 36px; +@border-radius: 4px; @terminal-header-height: 34px; @@ -259,7 +260,7 @@ h2 { .btn-opacity; cursor: pointer; padding: 4px 8px; - border-radius: 4px; + border-radius: @border-radius; opacity: 0.8; margin-bottom: 3px; @@ -943,48 +944,76 @@ h2 { .status { text-transform: uppercase; + padding: 2px 12px; + border-radius: @border-radius; + color: @text-secondary-color; + display: inline-block; &-icon { - font-size: 16px; + font-size: 1rem; position: relative; - top: 1px; - margin-right: 0.5em; + top: 0.125rem; + margin-right: 0.25rem; } &.status-loading { - animation: status-loading 2.0s infinite ease-in-out; + animation: blinking 2.0s infinite ease-in-out; text-transform: none; + color: @text-color; } } -.sidebar { - position: fixed; - bottom: 16px; - left: 16px; - font-size: .7rem; +.topology-options { - &-item { + .topology-option { color: @text-secondary-color; - border-radius: 2px; - padding: 2px 8px; - width: 100%; + margin: 6px 0; - &.status { - padding: 4px 8px; - margin-bottom: 4px; + &:last-child { + margin-bottom: 0; + } + + &-wrapper { + border-radius: @border-radius; + border: 1px solid @background-darker-color; + display: inline-block; } &-action { .btn-opacity; - text-transform: uppercase; - font-weight: bold; - color: darken(@weave-orange, 25%); + padding: 3px 12px; cursor: pointer; - font-size: 90%; - margin-left: 0.5em; - opacity: @link-opacity-default; + display: inline-block; + + &-selected, &:hover { + color: @text-darker-color; + background-color: @background-darker-color; + } + + &-selected { + cursor: default; + } + + &:first-child { + border-left: none; + border-top-left-radius: @border-radius; + border-bottom-left-radius: @border-radius; + } + + &:last-child { + border-top-right-radius: @border-radius; + border-bottom-right-radius: @border-radius; + } } } + +} + +.sidebar { + position: fixed; + bottom: 16px; + left: 16px; + font-size: .7rem; } @keyframes blinking { @@ -995,16 +1024,6 @@ h2 { } } -@keyframes status-loading { - 0%, 100% { - background-color: @background-darker-secondary-color; - color: @text-secondary-color; - } 50% { - background-color: @background-darker-color; - color: @text-color; - } -} - // // Debug panel! //