Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grid Mode #1673

Merged
merged 10 commits into from
Aug 3, 2016
Merged
31 changes: 31 additions & 0 deletions client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ export function showHelp() {
return {type: ActionTypes.SHOW_HELP};
}


export function hideHelp() {
return {type: ActionTypes.HIDE_HELP};
}


export function toggleHelp() {
return (dispatch, getState) => {
if (getState().get('showingHelp')) {
Expand All @@ -32,6 +34,18 @@ export function toggleHelp() {
};
}


export function sortOrderChanged(sortBy, sortedDesc) {

This comment was marked as abuse.

return (dispatch, getState) => {
dispatch({
type: ActionTypes.SORT_ORDER_CHANGED,
sortBy, sortedDesc
});
updateRoute(getState);
};
}


//
// Networks
//
Expand Down Expand Up @@ -83,6 +97,7 @@ export function unpinNetwork(networkId) {
// Metrics
//


export function selectMetric(metricId) {
return {
type: ActionTypes.SELECT_METRIC,
Expand Down Expand Up @@ -217,6 +232,22 @@ export function clickForceRelayout() {
};
}

export function toggleGridMode(enabledArgument) {

This comment was marked as abuse.

return (dispatch, getState) => {
const enabled = (enabledArgument === undefined) ?
!getState().get('gridMode') :
enabledArgument;
dispatch({
type: ActionTypes.SET_GRID_MODE,
enabled
});
updateRoute(getState);
if (!enabled) {
dispatch(clickForceRelayout());
}
};
}

export function clickNode(nodeId, label, origin) {
return (dispatch, getState) => {
dispatch({
Expand Down
29 changes: 10 additions & 19 deletions client/app/scripts/charts/nodes-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,12 @@ import { getActiveTopologyOptions, getAdjacentNodes,

const log = debug('scope:nodes-chart');

const MARGINS = {
top: 130,
left: 40,
right: 40,
bottom: 0
};

const ZOOM_CACHE_FIELDS = ['scale', 'panTranslateX', 'panTranslateY'];

// make sure circular layouts a bit denser with 3-6 nodes
const radiusDensity = d3.scale.threshold()
.domain([3, 6]).range([2.5, 3.5, 3]);
.domain([3, 6])
.range([2.5, 3.5, 3]);

class NodesChart extends React.Component {

Expand All @@ -47,8 +41,8 @@ class NodesChart extends React.Component {
scale: 1,
selectedNodeScale: d3.scale.linear(),
hasZoomed: false,
height: 0,
width: 0,
height: props.height || 0,
width: props.width || 0,
zoomCache: {}
};
}
Expand Down Expand Up @@ -237,9 +231,9 @@ class NodesChart extends React.Component {
// move origin node to center of viewport
const zoomScale = state.scale;
const translate = [state.panTranslateX, state.panTranslateY];
const centerX = (-translate[0] + (state.width + MARGINS.left
const centerX = (-translate[0] + (state.width + props.margins.left
- DETAILS_PANEL_WIDTH) / 2) / zoomScale;
const centerY = (-translate[1] + (state.height + MARGINS.top) / 2) / zoomScale;
const centerY = (-translate[1] + (state.height + props.margins.top) / 2) / zoomScale;
stateNodes = stateNodes.mergeIn([props.selectedNodeId], {
x: centerX,
y: centerY
Expand Down Expand Up @@ -310,9 +304,7 @@ class NodesChart extends React.Component {
}

updateGraphState(props, state) {
const n = props.nodes.size;

if (n === 0) {
if (props.nodes.size === 0) {
return {
nodes: makeMap(),
edges: makeMap()
Expand All @@ -328,7 +320,7 @@ class NodesChart extends React.Component {
width: state.width,
height: state.height,
scale: nodeScale,
margins: MARGINS,
margins: props.margins,
forceRelayout: props.forceRelayout,
topologyId: this.props.topologyId,
topologyOptions: this.props.topologyOptions
Expand All @@ -353,12 +345,12 @@ class NodesChart extends React.Component {
.map(edge => edge.set('ppoints', edge.get('points')));

// adjust layout based on viewport
const xFactor = (state.width - MARGINS.left - MARGINS.right) / graph.width;
const xFactor = (state.width - props.margins.left - props.margins.right) / graph.width;
const yFactor = state.height / graph.height;
const zoomFactor = Math.min(xFactor, yFactor);
let zoomScale = this.state.scale;

if (!state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
if (this.zoom && !state.hasZoomed && zoomFactor > 0 && zoomFactor < 1) {
zoomScale = zoomFactor;
// saving in d3's behavior cache
this.zoom.scale(zoomFactor);
Expand Down Expand Up @@ -402,7 +394,6 @@ function mapStateToProps(state) {
return {
adjacentNodes: getAdjacentNodes(state),
forceRelayout: state.get('forceRelayout'),
nodes: state.get('nodes').filter(node => !node.get('filtered')),
selectedNodeId: state.get('selectedNodeId'),
topologyId: state.get('currentTopologyId'),
topologyOptions: getActiveTopologyOptions(state)
Expand Down
159 changes: 159 additions & 0 deletions client/app/scripts/charts/nodes-grid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/* eslint react/jsx-no-bind: "off", no-multi-comp: "off" */

import React from 'react';
import { connect } from 'react-redux';
import { List as makeList, Map as makeMap } from 'immutable';
import NodeDetailsTable from '../components/node-details/node-details-table';
import { clickNode, sortOrderChanged } from '../actions/app-actions';

import { getNodeColor } from '../utils/color-utils';


const IGNORED_COLUMNS = ['docker_container_ports', 'docker_container_id', 'docker_image_id',

This comment was marked as abuse.

'docker_container_command', 'docker_container_networks'];


function getColumns(nodes) {
const metricColumns = nodes
.toList()
.flatMap(n => {
const metrics = (n.get('metrics') || makeList())
.map(m => makeMap({ id: m.get('id'), label: m.get('label') }));
return metrics;
})
.toSet()
.toList()
.sortBy(m => m.get('label'));

const metadataColumns = nodes
.toList()
.flatMap(n => {
const metadata = (n.get('metadata') || makeList())
.map(m => makeMap({ id: m.get('id'), label: m.get('label') }));
return metadata;
})
.toSet()
.filter(n => !IGNORED_COLUMNS.includes(n.get('id')))
.toList()
.sortBy(m => m.get('label'));

const relativesColumns = nodes
.toList()
.flatMap(n => {
const metadata = (n.get('parents') || makeList())
.map(m => makeMap({ id: m.get('topologyId'), label: m.get('topologyId') }));
return metadata;
})
.toSet()
.toList()
.sortBy(m => m.get('label'));

return relativesColumns.concat(metadataColumns, metricColumns).toJS();
}


function renderIdCell(props) {
const iconStyle = {
width: 16,
flex: 'none',
color: getNodeColor(props.rank, props.label_major)
};
const showSubLabel = Boolean(props.pseudo);

return (
<div title={props.label} className="nodes-grid-id-column">
<div style={iconStyle}><i className="fa fa-square" /></div>
<div className="truncate">
{props.label} {showSubLabel &&
<span className="nodes-grid-label-minor">{props.label_minor}</span>}
</div>
</div>
);
}


class NodesGrid extends React.Component {

constructor(props, context) {
super(props, context);

this.onClickRow = this.onClickRow.bind(this);
this.onSortChange = this.onSortChange.bind(this);
}

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

onSortChange(sortBy, sortedDesc) {
this.props.sortOrderChanged(sortBy, sortedDesc);
}

render() {
const { margins, nodes, height, gridSortBy, gridSortedDesc,
searchNodeMatches = makeMap(), searchQuery } = this.props;
const cmpStyle = {
height,
marginTop: margins.top,
paddingLeft: margins.left,
paddingRight: margins.right,
};
const tbodyHeight = height - 24 - 18;
const className = 'scroll-body';
const tbodyStyle = {
height: `${tbodyHeight}px`,
};

const detailsData = {
label: this.props.currentTopology && this.props.currentTopology.get('fullName'),
id: '',
nodes: nodes
.toList()
.filter(n => !searchQuery || searchNodeMatches.has(n.get('id')))
.toJS(),
columns: getColumns(nodes)
};

return (
<div className="nodes-grid">
{nodes.size > 0 && <NodeDetailsTable
style={cmpStyle}
className={className}
renderIdCell={renderIdCell}
tbodyStyle={tbodyStyle}
topologyId={this.props.currentTopologyId}
onSortChange={this.onSortChange}
onClickRow={this.onClickRow}
sortBy={gridSortBy}
sortedDesc={gridSortedDesc}
selectedNodeId={this.props.selectedNodeId}
limit={1000}
{...detailsData}
/>}
</div>
);
}
}


function mapStateToProps(state) {
return {
gridSortBy: state.get('gridSortBy'),
gridSortedDesc: state.get('gridSortedDesc'),
currentTopology: state.get('currentTopology'),
currentTopologyId: state.get('currentTopologyId'),
searchNodeMatches: state.getIn(['searchNodeMatches', state.get('currentTopologyId')]),
searchQuery: state.get('searchQuery'),
selectedNodeId: state.get('selectedNodeId')
};
}


export default connect(
mapStateToProps,
{ clickNode, sortOrderChanged }
)(NodesGrid);
17 changes: 11 additions & 6 deletions client/app/scripts/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import Topologies from './topologies.js';
import TopologyOptions from './topology-options.js';
import { getApiDetails, getTopologies } from '../utils/web-api-utils';
import { focusSearch, pinNextMetric, hitBackspace, hitEnter, hitEsc, unpinMetric,
selectMetric, toggleHelp } from '../actions/app-actions';
selectMetric, toggleHelp, toggleGridMode } from '../actions/app-actions';
import Details from './details';
import Nodes from './nodes';
import GridModeSelector from './grid-mode-selector';
import MetricSelector from './metric-selector';
import NetworkSelector from './networks-selector';
import EmbeddedTerminal from './embedded-terminal';
Expand Down Expand Up @@ -86,6 +87,8 @@ class App extends React.Component {
dispatch(pinNextMetric(-1));
} else if (char === '>') {
dispatch(pinNextMetric(1));
} else if (char === 't' || char === 'g') {
dispatch(toggleGridMode());
} else if (char === 'q') {
dispatch(unpinMetric());
dispatch(selectMetric(null));
Expand All @@ -99,8 +102,8 @@ class App extends React.Component {
}

render() {
const { showingDetails, showingHelp, showingMetricsSelector, showingNetworkSelector,
showingTerminal } = this.props;
const { gridMode, showingDetails, showingHelp, showingMetricsSelector,
showingNetworkSelector, showingTerminal } = this.props;
const isIframe = window !== window.top;

return (
Expand All @@ -125,10 +128,11 @@ class App extends React.Component {

<Nodes />

<Sidebar>
<Sidebar classNames={gridMode ? 'sidebar-gridmode' : ''}>
{showingMetricsSelector && !gridMode && <MetricSelector />}
{showingNetworkSelector && !gridMode && <NetworkSelector />}
<GridModeSelector />
<Status />
{showingMetricsSelector && <MetricSelector />}
{showingNetworkSelector && <NetworkSelector />}
<TopologyOptions />
</Sidebar>

Expand All @@ -141,6 +145,7 @@ class App extends React.Component {
function mapStateToProps(state) {
return {
activeTopologyOptions: getActiveTopologyOptions(state),
gridMode: state.get('gridMode'),
routeSet: state.get('routeSet'),
searchFocused: state.get('searchFocused'),
searchQuery: state.get('searchQuery'),
Expand Down
Loading