Topology filter overhaul, still needs backend support

This commit is contained in:
David Kaltschmidt
2016-03-11 13:00:49 +01:00
parent 430130c03a
commit 88abeb7168
8 changed files with 109 additions and 110 deletions

View File

@@ -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);

View File

@@ -26,6 +26,17 @@ function fromGraphNodeId(encodedId) {
return encodedId.replace('<DOT>', '.');
}
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;

View File

@@ -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} />
<Sidebar>

View File

@@ -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 (
<span className="sidebar-item-action" onClick={this.onClick}>
{this.props.value}
</span>
<div className={className} onClick={this.onClick}>
{item.get('label')}
</div>
);
}
}

View File

@@ -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 (
<TopologyOptionAction option={option} value={action} topologyId={topologyId} key={action} />
);
}
/**
* 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 (
<div className="sidebar-item" key={option}>
{activeText}
<span className="sidebar-item-actions">
{actions}
</span>
<div className="topology-option" key={optionId}>
<div className="topology-option-wrapper">
{option.get('options').map(item => <TopologyOptionAction
optionId={optionId} topologyId={topologyId} key={item.get('value')}
activeValue={activeValue} item={item} />)}
</div>
</div>
);
}
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 (
<div className="topology-options">
{options.toIndexedSeq().map(items => this.renderOption(items))}
{this.props.options.toIndexedSeq().map(option => this.renderOption(option))}
</div>
);
}

View File

@@ -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', () => {

View File

@@ -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);
});
}

View File

@@ -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;
}
}
.topology-options {
.topology-option {
color: @text-secondary-color;
margin: 6px 0;
&:last-child {
margin-bottom: 0;
}
&-wrapper {
border-radius: @border-radius;
border: 1px solid @background-darker-color;
display: inline-block;
}
&-action {
.btn-opacity;
padding: 3px 12px;
cursor: pointer;
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;
&-item {
color: @text-secondary-color;
border-radius: 2px;
padding: 2px 8px;
width: 100%;
&.status {
padding: 4px 8px;
margin-bottom: 4px;
}
&-action {
.btn-opacity;
text-transform: uppercase;
font-weight: bold;
color: darken(@weave-orange, 25%);
cursor: pointer;
font-size: 90%;
margin-left: 0.5em;
opacity: @link-opacity-default;
}
}
}
@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!
//