mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 02:00:43 +00:00
Topology filter overhaul, still needs backend support
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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!
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user