Skip to content

Commit

Permalink
Merge pull request #752 from weaveworks/301-details-pain
Browse files Browse the repository at this point in the history
Details panel redesign
  • Loading branch information
paulbellamy committed Jan 19, 2016
2 parents 359ec29 + c43abd6 commit 0afd151
Show file tree
Hide file tree
Showing 82 changed files with 3,240 additions and 1,424 deletions.
5 changes: 3 additions & 2 deletions app/api_topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/gorilla/websocket"

"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/render/detailed"
)

const (
Expand All @@ -21,7 +22,7 @@ type APITopology struct {

// APINode is returned by the /api/topology/{name}/{id} handler.
type APINode struct {
Node render.DetailedNode `json:"node"`
Node detailed.Node `json:"node"`
}

// Full topology.
Expand Down Expand Up @@ -59,7 +60,7 @@ func handleNode(nodeID string) func(Reporter, render.Renderer, http.ResponseWrit
http.NotFound(w, r)
return
}
respondWith(w, http.StatusOK, APINode{Node: render.MakeDetailedNode(rpt, node)})
respondWith(w, http.StatusOK, APINode{Node: detailed.MakeNode(rpt, node)})
}
}

Expand Down
9 changes: 3 additions & 6 deletions app/api_topology_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ func TestAPITopologyApplications(t *testing.T) {
t.Fatal(err)
}
equals(t, expected.ServerProcessID, node.Node.ID)
equals(t, "apache", node.Node.LabelMajor)
equals(t, fmt.Sprintf("%s (server:%s)", fixture.ServerHostID, fixture.ServerPID), node.Node.LabelMinor)
equals(t, "apache", node.Node.Label)
equals(t, false, node.Node.Pseudo)
// Let's not unit-test the specific content of the detail tables
}
Expand All @@ -96,8 +95,7 @@ func TestAPITopologyApplications(t *testing.T) {
t.Fatal(err)
}
equals(t, fixture.Client1Name, node.Node.ID)
equals(t, fixture.Client1Name, node.Node.LabelMajor)
equals(t, "2 processes", node.Node.LabelMinor)
equals(t, fixture.Client1Name, node.Node.Label)
equals(t, false, node.Node.Pseudo)
// Let's not unit-test the specific content of the detail tables
}
Expand Down Expand Up @@ -125,8 +123,7 @@ func TestAPITopologyHosts(t *testing.T) {
t.Fatal(err)
}
equals(t, expected.ServerHostRenderedID, node.Node.ID)
equals(t, "server", node.Node.LabelMajor)
equals(t, "hostname.com", node.Node.LabelMinor)
equals(t, "server", node.Node.Label)
equals(t, false, node.Node.Pseudo)
// Let's not unit-test the specific content of the detail tables
}
Expand Down
71 changes: 59 additions & 12 deletions client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,22 @@ export function changeTopologyOption(option, value, topologyId) {
AppStore.getActiveTopologyOptions()
);
getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
AppStore.getTopologyUrlsById(),
AppStore.getNodeDetails()
);
}

export function clickCloseDetails() {
export function clickBackground() {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_BACKGROUND
});
updateRoute();
}

export function clickCloseDetails(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_CLOSE_DETAILS
type: ActionTypes.CLICK_CLOSE_DETAILS,
nodeId
});
updateRoute();
}
Expand All @@ -49,15 +57,45 @@ export function clickCloseTerminal(pipeId, closePipe) {
updateRoute();
}

export function clickNode(nodeId) {
export function clickNode(nodeId, label, origin) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_NODE,
nodeId: nodeId
origin,
label,
nodeId
});
updateRoute();
getNodeDetails(
AppStore.getTopologyUrlsById(),
AppStore.getNodeDetails()
);
}

export function clickRelative(nodeId, topologyId, label, origin) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_RELATIVE,
label,
origin,
nodeId,
topologyId
});
updateRoute();
getNodeDetails(
AppStore.getTopologyUrlsById(),
AppStore.getNodeDetails()
);
}

export function clickShowTopologyForNode(topologyId, nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE,
topologyId,
nodeId
});
updateRoute();
getNodesDelta(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
AppStore.getActiveTopologyOptions()
);
}

Expand Down Expand Up @@ -121,7 +159,7 @@ export function hitEsc() {
type: ActionTypes.CLICK_CLOSE_TERMINAL,
pipeId: controlPipe.id
});
// Dont deselect node on ESC if there is a controlPipe (keep terminal open)
// Dont deselect node on ESC if there is a controlPipe (keep terminal open)
} else if (AppStore.getSelectedNodeId() && !controlPipe) {
AppDispatcher.dispatch({type: ActionTypes.DESELECT_NODE});
}
Expand Down Expand Up @@ -181,8 +219,8 @@ export function receiveTopologies(topologies) {
AppStore.getActiveTopologyOptions()
);
getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
AppStore.getTopologyUrlsById(),
AppStore.getNodeDetails()
);
}

Expand All @@ -195,6 +233,7 @@ export function receiveApiDetails(apiDetails) {
}

export function receiveControlPipeFromParams(pipeId, rawTty) {
// TODO add nodeId
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_CONTROL_PIPE,
pipeId: pipeId,
Expand All @@ -216,6 +255,7 @@ export function receiveControlPipe(pipeId, nodeId, rawTty) {

AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_CONTROL_PIPE,
nodeId: nodeId,
pipeId: pipeId,
rawTty: rawTty
});
Expand All @@ -238,6 +278,13 @@ export function receiveError(errorUrl) {
});
}

export function receiveNotFound(nodeId) {
AppDispatcher.dispatch({
nodeId,
type: ActionTypes.RECEIVE_NOT_FOUND
});
}

export function route(state) {
AppDispatcher.dispatch({
state: state,
Expand All @@ -251,7 +298,7 @@ export function route(state) {
AppStore.getActiveTopologyOptions()
);
getNodeDetails(
AppStore.getCurrentTopologyUrl(),
AppStore.getSelectedNodeId()
AppStore.getTopologyUrlsById(),
AppStore.getNodeDetails()
);
}
11 changes: 6 additions & 5 deletions client/app/scripts/charts/node.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Motion, spring } from 'react-motion';

import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
Expand Down Expand Up @@ -99,14 +100,14 @@ export default class Node extends React.Component {

handleMouseClick(ev) {
ev.stopPropagation();
clickNode(ev.currentTarget.id);
clickNode(this.props.id, this.props.label, ReactDOM.findDOMNode(this).getBoundingClientRect());
}

handleMouseEnter(ev) {
enterNode(ev.currentTarget.id);
handleMouseEnter() {
enterNode(this.props.id);
}

handleMouseLeave(ev) {
leaveNode(ev.currentTarget.id);
handleMouseLeave() {
leaveNode(this.props.id);
}
}
4 changes: 2 additions & 2 deletions client/app/scripts/charts/nodes-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import { Map as makeMap } from 'immutable';
import timely from 'timely';

import { clickCloseDetails } from '../actions/app-actions';
import { clickBackground } from '../actions/app-actions';
import AppStore from '../stores/app-store';
import Edge from './edge';
import { EDGE_ID_SEPARATOR } from '../constants/naming';
Expand Down Expand Up @@ -357,7 +357,7 @@ export default class NodesChart extends React.Component {

handleMouseClick() {
if (!this.isZooming) {
clickCloseDetails();
clickBackground();
} else {
this.isZooming = false;
}
Expand Down
7 changes: 5 additions & 2 deletions client/app/scripts/components/__tests__/node-details-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import TestUtils from 'react/lib/ReactTestUtils';

jest.dontMock('../../dispatcher/app-dispatcher');
jest.dontMock('../node-details.js');
jest.dontMock('../node-details/node-details-controls.js');
jest.dontMock('../node-details/node-details-relatives.js');
jest.dontMock('../node-details/node-details-table.js');
jest.dontMock('../../utils/color-utils');
jest.dontMock('../../utils/title-utils');

Expand All @@ -22,14 +25,14 @@ describe('NodeDetails', () => {
});

it('shows n/a when node was not found', () => {
const c = TestUtils.renderIntoDocument(<NodeDetails nodes={nodes} nodeId={nodeId} />);
const c = TestUtils.renderIntoDocument(<NodeDetails notFound />);
const notFound = TestUtils.findRenderedDOMComponentWithClass(c, 'node-details-header-notavailable');
expect(notFound).toBeDefined();
});

it('show label of node with title', () => {
nodes = nodes.set(nodeId, Immutable.fromJS({id: nodeId}));
details = {label_major: 'Node 1', tables: []};
details = {label: 'Node 1'};
const c = TestUtils.renderIntoDocument(<NodeDetails nodes={nodes}
nodeId={nodeId} details={details} />);

Expand Down
9 changes: 4 additions & 5 deletions client/app/scripts/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ function getStateFromStores() {
highlightedEdgeIds: AppStore.getHighlightedEdgeIds(),
highlightedNodeIds: AppStore.getHighlightedNodeIds(),
hostname: AppStore.getHostname(),
selectedNodeId: AppStore.getSelectedNodeId(),
nodeDetails: AppStore.getNodeDetails(),
nodes: AppStore.getNodes(),
selectedNodeId: AppStore.getSelectedNodeId(),
topologies: AppStore.getTopologies(),
topologiesLoaded: AppStore.isTopologiesLoaded(),
version: AppStore.getVersion(),
Expand Down Expand Up @@ -70,7 +70,7 @@ export default class App extends React.Component {
}

render() {
const showingDetails = this.state.selectedNodeId;
const showingDetails = this.state.nodeDetails.size > 0;
const showingTerminal = this.state.controlPipe;
const footer = `Version ${this.state.version} on ${this.state.hostname}`;
// width of details panel blocking a view
Expand All @@ -81,13 +81,12 @@ export default class App extends React.Component {
<div className="app">
{showingDebugToolbar() && <DebugToolbar />}
{showingDetails && <Details nodes={this.state.nodes}
nodeId={this.state.selectedNodeId}
controlStatus={this.state.controlStatus[this.state.selectedNodeId]}
controlStatus={this.state.controlStatus}
details={this.state.nodeDetails} />}

{showingTerminal && <EmbeddedTerminal
pipe={this.state.controlPipe}
nodeId={this.state.selectedNodeId}
nodeId={this.state.controlPipe.nodeId}
nodes={this.state.nodes} />}

<div className="header">
Expand Down
58 changes: 58 additions & 0 deletions client/app/scripts/components/details-card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';

import NodeDetails from './node-details';

// card dimensions in px
const marginTop = 24;
const marginBottom = 48;
const marginRight = 36;
const panelWidth = 420;
const offset = 8;

export default class DetailsCard extends React.Component {

constructor(props, context) {
super(props, context);
this.state = {
mounted: null
};
}

componentDidMount() {
setTimeout(() => {
this.setState({mounted: true});
});
}

render() {
let transform;
const origin = this.props.origin;
const panelHeight = window.innerHeight - marginBottom - marginTop;
if (origin && !this.state.mounted) {
// render small panel near origin, will transition into normal panel after being mounted
const scaleY = origin.height / (window.innerHeight - marginBottom - marginTop) / 2;
const scaleX = origin.width / panelWidth / 2;
const centerX = window.innerWidth - marginRight - (panelWidth / 2);
const centerY = (panelHeight) / 2 + marginTop;
const dx = (origin.left + origin.width / 2) - centerX;
const dy = (origin.top + origin.height / 2) - centerY;
transform = `translate(${dx}px, ${dy}px) scale(${scaleX},${scaleY})`;
} else {
// stack effect: shift top cards to the left, shrink lower cards vertically
const shiftX = -1 * this.props.index * offset;
const position = this.props.cardCount - this.props.index - 1; // reverse index
const scaleY = position === 0 ? 1 : (panelHeight - 2 * offset * position) / panelHeight;
if (scaleY !== 1) {
transform = `translateX(${shiftX}px) scaleY(${scaleY})`;
} else {
// scale(1) is sometimes blurry
transform = `translateX(${shiftX}px)`;
}
}
return (
<div className="details-wrapper" style={{transform}}>
<NodeDetails nodeId={this.props.id} key={this.props.id} {...this.props} />
</div>
);
}
}
31 changes: 11 additions & 20 deletions client/app/scripts/components/details.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
import React from 'react';

import { clickCloseDetails } from '../actions/app-actions';
import NodeDetails from './node-details';
import DetailsCard from './details-card';

export default class Details extends React.Component {
constructor(props, context) {
super(props, context);
this.handleClickClose = this.handleClickClose.bind(this);
}

handleClickClose(ev) {
ev.preventDefault();
clickCloseDetails();
}

// render all details as cards, later cards go on top
render() {
const details = this.props.details.toIndexedSeq();
return (
<div id="details">
<div className="details-wrapper">
<div className="details-tools-wrapper">
<div className="details-tools">
<span className="fa fa-close" onClick={this.handleClickClose} />
</div>
</div>
<NodeDetails {...this.props} />
</div>
<div className="details">
{details.map((obj, index) => {
return (
<DetailsCard key={obj.id} index={index} cardCount={details.size}
nodes={this.props.nodes}
nodeControlStatus={this.props.controlStatus[obj.id]} {...obj} />
);
})}
</div>
);
}
Expand Down
Loading

0 comments on commit 0afd151

Please sign in to comment.