diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 3ce0c85f3..301b36576 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -12,7 +12,8 @@ import { DETAILS_PANEL_WIDTH } from '../constants/styles'; import Logo from '../components/logo'; import { doLayout } from './nodes-layout'; import NodesChartElements from './nodes-chart-elements'; -import { getActiveTopologyOptions, getAdjacentNodes } from '../utils/topology-utils'; +import { getActiveTopologyOptions, getAdjacentNodes, + isSameTopology } from '../utils/topology-utils'; const log = debug('scope:nodes-chart'); @@ -40,8 +41,8 @@ class NodesChart extends React.Component { scale: 1, selectedNodeScale: d3.scale.linear(), hasZoomed: false, - height: props.height || 0, - width: props.width || 0, + height: 0, + width: 0, zoomCache: {} }; } @@ -60,7 +61,7 @@ class NodesChart extends React.Component { // re-apply cached canvas zoom/pan to d3 behavior (or set defaul values) const defaultZoom = { scale: 1, panTranslateX: 0, panTranslateY: 0, hasZoomed: false }; const nextZoom = this.state.zoomCache[nextProps.topologyId] || defaultZoom; - if (this.zoom && nextZoom) { + if (nextZoom) { this.zoom.scale(nextZoom.scale); this.zoom.translate([nextZoom.panTranslateX, nextZoom.panTranslateY]); } @@ -78,13 +79,11 @@ class NodesChart extends React.Component { } // reset layout dimensions only when forced - // state.height = nextProps.height; - // state.width = nextProps.width; state.height = nextProps.forceRelayout ? nextProps.height : (state.height || nextProps.height); state.width = nextProps.forceRelayout ? nextProps.width : (state.width || nextProps.width); // _.assign(state, this.updateGraphState(nextProps, state)); - if (nextProps.forceRelayout || nextProps.nodes !== this.props.nodes) { + if (nextProps.forceRelayout || !isSameTopology(nextProps.nodes, this.props.nodes)) { _.assign(state, this.updateGraphState(nextProps, state)); } @@ -100,10 +99,6 @@ class NodesChart extends React.Component { componentDidMount() { // distinguish pan/zoom from click - if (this.props.noZoom) { - return; - } - this.isZooming = false; this.zoom = d3.behavior.zoom() @@ -115,10 +110,6 @@ class NodesChart extends React.Component { } componentWillUnmount() { - if (this.props.noZoom) { - return; - } - // undoing .call(zoom) d3.select('.nodes-chart svg') .on('mousedown.zoom', null) @@ -294,10 +285,8 @@ class NodesChart extends React.Component { restoreLayout(state) { // undo any pan/zooming that might have happened - if (this.zoom) { - this.zoom.scale(state.scale); - this.zoom.translate([state.panTranslateX, state.panTranslateY]); - } + this.zoom.scale(state.scale); + this.zoom.translate([state.panTranslateX, state.panTranslateY]); const nodes = state.nodes.map(node => node.merge({ x: node.get('px'), @@ -326,10 +315,6 @@ class NodesChart extends React.Component { const stateEdges = this.initEdges(props.nodes, stateNodes); const nodeScale = this.getNodeScale(props.nodes, state.width, state.height); const nextState = { nodeScale }; - const nodeOrder = props.nodeOrder || makeMap(stateNodes - .toList() - .sortBy(n => n.get('label')) - .map((n, i) => [n.get('id'), i])); const options = { width: state.width, @@ -338,8 +323,7 @@ class NodesChart extends React.Component { margins: props.margins, forceRelayout: props.forceRelayout, topologyId: this.props.topologyId, - topologyOptions: this.props.topologyOptions, - nodeOrder + topologyOptions: this.props.topologyOptions }; const timedLayouter = timely(doLayout); @@ -366,12 +350,10 @@ class NodesChart extends React.Component { const zoomFactor = Math.min(xFactor, yFactor); let zoomScale = this.state.scale; - if (!this.props.noZoom && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) { + if (!state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) { zoomScale = zoomFactor; // saving in d3's behavior cache - if (this.zoom) { - this.zoom.scale(zoomFactor); - } + this.zoom.scale(zoomFactor); } nextState.scale = zoomScale; @@ -390,7 +372,7 @@ class NodesChart extends React.Component { const nodeSize = expanse / 3; // single node should fill a third of the screen const maxNodeSize = expanse / 10; const normalizedNodeSize = Math.min(nodeSize / Math.sqrt(nodes.size), maxNodeSize); - return this.state.nodeScale.copy().range([0, this.props.nodeSize || normalizedNodeSize]); + return this.state.nodeScale.copy().range([0, normalizedNodeSize]); } zoomed() { diff --git a/client/app/scripts/charts/nodes-grid.js b/client/app/scripts/charts/nodes-grid.js index e78a2a9bd..8e791a3c6 100644 --- a/client/app/scripts/charts/nodes-grid.js +++ b/client/app/scripts/charts/nodes-grid.js @@ -54,20 +54,19 @@ function getColumns(nodes) { function renderIdCell(props) { - const style = { + const iconStyle = { width: 16, flex: 'none', color: getNodeColor(props.rank, props.label_major) }; + const showSubLabel = Boolean(props.pseudo); return (
-
-
-
- {props.label} {props.pseudo && - {props.label_minor}} -
+
+
+ {props.label} {showSubLabel && + {props.label_minor}}
); @@ -79,33 +78,18 @@ class NodesGrid extends React.Component { constructor(props, context) { super(props, context); - this.renderIdCell = this.renderIdCell.bind(this); - this.clickRow = this.clickRow.bind(this); - + this.onClickRow = this.onClickRow.bind(this); this.onSortChange = this.onSortChange.bind(this); - this.onMouseEnterRow = this.onMouseEnterRow.bind(this); - this.onMouseLeaveRow = this.onMouseLeaveRow.bind(this); } - clickRow(ev, node, el) { + onClickRow(ev, node, el) { + // TODO: do this better if (ev.target.className === 'node-details-table-node-link') { return; } this.props.clickNode(node.id, node.label, el.getBoundingClientRect()); } - renderIdCell(props) { - return renderIdCell(props); - } - - onMouseEnterRow() { - this.props.clickPauseUpdate(); - } - - onMouseLeaveRow() { - this.props.clickResumeUpdate(); - } - onSortChange(sortBy, sortedDesc) { this.props.sortOrderChanged(sortBy, sortedDesc); } @@ -140,16 +124,17 @@ class NodesGrid extends React.Component { + limit={1000} + {...detailsData} + />
); } diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index 0c0392466..116e35f4d 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -1,6 +1,5 @@ import dagre from 'dagre'; import debug from 'debug'; -import d3 from 'd3'; import { fromJS, Map as makeMap, Set as ImmSet } from 'immutable'; import { EDGE_ID_SEPARATOR } from '../constants/naming'; @@ -50,9 +49,7 @@ function runLayoutEngine(graph, imNodes, imEdges, opts) { // configure node margins graph.setGraph({ nodesep, - ranksep, - // rankdir: 'LR', - // align: 'UL' + ranksep }); // add nodes to the graph if not already there @@ -179,17 +176,18 @@ function layoutSingleNodes(layout, opts) { offsetY = offsetY || margins.top + nodeHeight / 2; const columns = Math.ceil(Math.sqrt(singleNodes.size)); - const rows = Math.ceil(singleNodes.size / columns); let row = 0; let col = 0; + let singleX; + let singleY; nodes = nodes.sortBy(node => node.get('rank')).map(node => { if (singleNodes.has(node.get('id'))) { if (col === columns) { col = 0; row++; } - const singleX = col * (nodesep + nodeWidth) + offsetX; - const singleY = row * (ranksep + nodeHeight) + offsetY; + singleX = col * (nodesep + nodeWidth) + offsetX; + singleY = row * (ranksep + nodeHeight) + offsetY; col++; return node.merge({ x: singleX, @@ -200,8 +198,8 @@ function layoutSingleNodes(layout, opts) { }); // adjust layout dimensions if graph is now bigger - result.width = Math.max(layout.width, columns * nodeWidth + (columns - 1) * nodesep); - result.height = Math.max(layout.height, rows * nodeHeight + (rows - 1) * ranksep); + result.width = Math.max(layout.width, singleX + nodeWidth / 2 + nodesep); + result.height = Math.max(layout.height, singleY + nodeHeight / 2 + ranksep); result.nodes = nodes; } @@ -246,7 +244,6 @@ function shiftLayoutToCenter(layout, opts) { return result; } - /** * Adds `points` array to edge based on location of source and target * @param {Map} edge new edge @@ -262,39 +259,6 @@ function setSimpleEdgePoints(edge, nodeCache) { ])); } - -export function uniqueRowConstraint(layout, options) { - const result = Object.assign({}, layout); - const scale = options.scale || DEFAULT_SCALE; - const nodeHeight = scale(NODE_SIZE_FACTOR); - const nodeWidth = scale(NODE_SIZE_FACTOR); - const margins = options.margins || DEFAULT_MARGINS; - - const rowHeight = options.height / layout.nodes.size; - const nodeOrder = options.nodeOrder || makeMap(layout.nodes - .toList() - .sortBy(n => n.get('y')) - .map((n, i) => [n.get('id'), i])); - - const nodeXs = layout.nodes.map(n => n.get('x')).toList().toJS(); - const xScale = d3.scale.linear() - .domain(d3.extent(nodeXs)) - .range([nodeWidth, options.width - nodeWidth]) - .clamp(false); - - result.nodes = layout.nodes.map(node => node.merge({ - x: xScale(node.get('x')), - y: nodeOrder.get(node.get('id')) * rowHeight + nodeHeight * 0.5 + margins.top + 2 - })); - - result.edges = layout.edges.map(edge => ( - setSimpleEdgePoints(edge, result.nodes) - )); - - return result; -} - - /** * Determine if nodes were added between node sets * @param {Map} nodes new Map of nodes @@ -391,7 +355,6 @@ export function doLayout(immNodes, immEdges, opts) { let layout; ++layoutRuns; - // if (false && !options.forceRelayout && cachedLayout && nodeCache && edgeCache if (!options.forceRelayout && cachedLayout && nodeCache && edgeCache && !hasUnseenNodes(immNodes, nodeCache)) { log('skip layout, trivial adjustment', ++layoutRunsTrivial, layoutRuns); @@ -407,7 +370,6 @@ export function doLayout(immNodes, immEdges, opts) { } layout = layoutSingleNodes(layout, opts); layout = shiftLayoutToCenter(layout, opts); - // layout = uniqueRowConstraint(layout, opts); } // cache results diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 8d2a4e703..ae61fff59 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -184,7 +184,7 @@ class DebugToolbar extends React.Component { addNodes(n, prefix = 'zing') { const ns = this.props.nodes; const nodeNames = ns.keySeq().toJS(); - const newNodeNames = _.range(nodeNames.length, nodeNames.length + n).map(i => ( + const newNodeNames = _.range(ns.size, ns.size + n).map(i => ( // `${randomLetter()}${randomLetter()}-zing` `${prefix}${i}` )); diff --git a/client/app/scripts/components/examples.js b/client/app/scripts/components/examples.js deleted file mode 100644 index 43a82b439..000000000 --- a/client/app/scripts/components/examples.js +++ /dev/null @@ -1,150 +0,0 @@ -/* eslint no-unused-vars: "off" */ -import React from 'react'; -import _ from 'lodash'; -import NodesChart from '../charts/nodes-chart'; -import NodesGrid from '../charts/nodes-grid'; -import { deltaAdd, makeNodes } from './debug-toolbar'; -import { fromJS, Map as makeMap, Set as makeSet } from 'immutable'; - - -function clog(v) { - console.log(v); - return v; -} - -function randomGraph(n) { - return makeMap(makeNodes(n, 'ewq', 4, 'hexagon').map(d => [d.id, fromJS(d)])); -} - -function deltaAddSimple(name, adjacency = []) { - return deltaAdd(name, adjacency, 'circle', false, 1, ''); -} - - -function makeIds(n) { - return _.range(n).map(i => `n${i}`); -} - - -function disconnectedGraph(n) { - return makeMap(makeIds(n) - .map((id) => deltaAddSimple(id)) - .map(d => [d.id, fromJS(d)])); -} - - -function completeGraph(n) { - const ids = makeIds(n); - const allEdges = _.flatMap(ids, i => ids.filter(ii => i !== ii).map(ii => [i, ii])); - const oneWayEdges = allEdges.filter(edge => _.isEqual(edge, _.sortBy(edge))); - const adjacencyMap = _(oneWayEdges) - .groupBy(e => e[0]) - .mapValues(edges => edges.map(e => e[1])) - .value(); - return makeMap(ids - .map((id) => deltaAddSimple(id, adjacencyMap[id] || [])) - .map(d => [d.id, fromJS(d)])); -} - - -function completeGraphBi(n) { - const ids = makeIds(n); - const adjacency = (id) => ids.filter(_id => _id !== id); - return makeMap(ids - .map((id) => deltaAddSimple(id, adjacency(id))) - .map(d => [d.id, fromJS(d)])); -} - - -function flatTree(n) { - const ids = makeIds(n + 1); - const p = ids.pop(); - const adjacency = id => id === p ? ids : []; - return makeMap(ids.concat([p]) - .map((id) => deltaAddSimple(id, adjacency(id))) - .map(d => [d.id, fromJS(d)])); -} - -function proxyGraph(n) { - const ids = makeIds(n * 2 + 1); - const p = ids.pop(); - const topIds = _.take(ids, n); - const bottomIds = _.drop(ids, n); - const adjacencyMap = Object.assign({ - [p]: bottomIds - }, _.fromPairs(topIds.map(id => [id, [p]]))); - - return makeMap(ids.concat([p]) - .map((id) => deltaAddSimple(id, adjacencyMap[id] || [])) - .map(d => [d.id, fromJS(d)])); -} - - -function chart(nodes, - n, - style = { width: 250, height: 250 }, - margins = { top: 0, left: 0, right: 0, bottom: 0 }, - nodeSize = null) { - return ( -
- -
- ); -} - - -function variants() { - const nCharts = 5; - const width = 250; - const style = {width: nCharts * (width + 16)}; - const generators = [ - { id: 'disconnectedGraph', fn: disconnectedGraph }, - { id: 'completeGraphBi', fn: completeGraphBi }, - { id: 'completeGraph', fn: completeGraph }, - { id: 'flatTree', fn: flatTree }, - { id: 'proxyGraph', fn: proxyGraph } - ]; - return ( -
- {_.reverse(generators).map(({id, fn}) => ( -
- {_.range(1, nCharts + 1).map(i => ( - chart(fn(i), i) - ))} -
- ))} -
- ); -} - - -function gridView() { - const nodes = randomGraph(50).map(node => node.remove('label').remove('label_minor')); - const nodeSize = 24; - return ( - - ); -} - - -export class Examples extends React.Component { - render() { - return ( -
- {gridView()} - {false && variants()} -
- ); - } -} diff --git a/client/app/scripts/components/node-details/node-details-table.js b/client/app/scripts/components/node-details/node-details-table.js index cd9f17fbf..d8bfc32ff 100644 --- a/client/app/scripts/components/node-details/node-details-table.js +++ b/client/app/scripts/components/node-details/node-details-table.js @@ -1,6 +1,5 @@ import _ from 'lodash'; import React from 'react'; -import { Map as makeMap } from 'immutable'; import classNames from 'classnames'; import ShowMore from '../show-more'; @@ -69,7 +68,6 @@ function getValueForSortBy(sortBy) { } -/* function getMetaDataSorters(nodes) { // returns an array of sorters that will take a node return _.get(nodes, [0, 'metadata'], []).map((field, index) => node => { @@ -83,15 +81,14 @@ function getMetaDataSorters(nodes) { return null; }); } -*/ function getSortedNodes(nodes, columns, sortBy, sortedDesc) { const sortedNodes = _.sortBy( nodes, getValueForSortBy(sortBy || getDefaultSortBy(columns, nodes)), - 'label' - // getMetaDataSorters(nodes) + 'label', + getMetaDataSorters(nodes) ); if (sortedDesc) { sortedNodes.reverse(); @@ -217,12 +214,6 @@ export default class NodeDetailsTable extends React.Component { nodes = nodes.slice(0, this.state.limit); } - const nodeOrderJS = (nodes || []).map((n, i) => [n.id, i]); - const nodeOrder = makeMap(nodeOrderJS); - const childrenWithProps = React.Children.map(this.props.children, (child) => ( - React.cloneElement(child, { nodeOrder }) - )); - const className = classNames('node-details-table-wrapper-wrapper', this.props.className); return ( @@ -257,7 +248,6 @@ export default class NodeDetailsTable extends React.Component { expanded={expanded} notShown={notShown} /> - {childrenWithProps} ); } diff --git a/client/app/scripts/examples-main.js b/client/app/scripts/examples-main.js deleted file mode 100644 index 562e2f508..000000000 --- a/client/app/scripts/examples-main.js +++ /dev/null @@ -1,10 +0,0 @@ -require('../styles/main.less'); -require('../../node_modules/fixed-data-table/dist/fixed-data-table.css'); -require('../images/favicon.ico'); - -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { Examples } from './components/examples.js'; - -ReactDOM.render(, document.getElementById('app')); diff --git a/client/app/styles/main.less b/client/app/styles/main.less index ee97b40fb..e58463a9e 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -1508,24 +1508,9 @@ h2 { } // -// Examples +// Nodes grid. // -.examples { - background-color: @background-average-color; - .example-chart { - position: relative; - } -} - -.nodes-chart-examples { - .example-chart { - margin: 8px; - display: inline-block; - border: 1px solid steelBlue; - } -} - .nodes-grid { tr { @@ -1538,19 +1523,16 @@ h2 { &-id-column { margin: -3px -4px; - padding: 2px 2px; - - .content { - display: flex; - div { - flex: 1; - } + padding: 2px 4px; + display: flex; + div { + flex: 1; } } .node-details-table-wrapper-wrapper { - flex: 1; + flex: 1; display: flex; flex-direction: row; width: 100%; diff --git a/client/webpack.local.config.js b/client/webpack.local.config.js index 40d1a96c9..4698cb5cb 100644 --- a/client/webpack.local.config.js +++ b/client/webpack.local.config.js @@ -15,13 +15,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin'); */ // Inject websocket url to dev backend - -var WEBPACK_SERVER_HOST = process.env.WEBPACK_SERVER_HOST || 'localhost'; -var COMMON_DEPS = [ - 'webpack-dev-server/client?http://' + WEBPACK_SERVER_HOST + ':4041', - 'webpack/hot/only-dev-server', - './app/scripts/debug' -]; + var WEBPACK_SERVER_HOST = process.env.WEBPACK_SERVER_HOST || 'localhost'; module.exports = { @@ -69,11 +63,6 @@ module.exports = { new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin(), new webpack.IgnorePlugin(/^\.\/locale$/, [/moment$/]), - new HtmlWebpackPlugin({ - chunks: ['vendors', 'examples-app'], - template: 'app/html/index.html', - filename: 'examples.html' - }), new HtmlWebpackPlugin({ chunks: ['vendors', 'contrast-app'], template: 'app/html/index.html', @@ -115,7 +104,7 @@ module.exports = { loader: 'json-loader' }, { - test: /(\.css|\.less)$/, + test: /\.less$/, loader: 'style-loader!css-loader!postcss-loader!less-loader' }, {