Skip to content

Commit

Permalink
Terminal UI for pipes
Browse files Browse the repository at this point in the history
  • Loading branch information
davkal committed Nov 27, 2015
1 parent a1195c5 commit 9d72ee8
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 48 deletions.
23 changes: 22 additions & 1 deletion client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ module.exports = {
RouterUtils.updateRoute();
},

clickCloseTerminal: function(pipeId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_CLOSE_TERMINAL,
pipeId: pipeId
});
},

clickNode: function(nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_NODE,
Expand All @@ -48,6 +55,13 @@ module.exports = {
);
},

clickTerminal: function(pipeId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_TERMINAL,
pipeId: pipeId
});
},

clickTopology: function(topologyId) {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_TOPOLOGY,
Expand Down Expand Up @@ -79,6 +93,12 @@ module.exports = {
},

doControl: function(probeId, nodeId, control) {
// TODO remove this fake dispatch
// AppDispatcher.dispatch({
// type: ActionTypes.RECEIVE_CONTROL_PIPE,
// nodeId: nodeId,
// pipeId: nodeId + control
// });
AppDispatcher.dispatch({
type: ActionTypes.DO_CONTROL
});
Expand Down Expand Up @@ -131,9 +151,10 @@ module.exports = {
});
},

receiveControlPipe: function(pipeId) {
receiveControlPipe: function(pipeId, nodeId) {
AppDispatcher.dispatch({
type: ActionTypes.RECEIVE_CONTROL_PIPE,
nodeId: nodeId,
pipeId: pipeId
});
},
Expand Down
11 changes: 8 additions & 3 deletions client/app/scripts/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function getStateFromStores() {
activeTopologyOptions: AppStore.getActiveTopologyOptions(),
controlError: AppStore.getControlError(),
controlPending: AppStore.isControlPending(),
controlPipe: AppStore.getControlPipe(),
controlPipes: AppStore.getControlPipes(),
currentTopology: AppStore.getCurrentTopology(),
currentTopologyId: AppStore.getCurrentTopologyId(),
currentTopologyOptions: AppStore.getCurrentTopologyOptions(),
Expand Down Expand Up @@ -74,14 +74,19 @@ const App = React.createClass({
const topMargin = 100;

return (
<div>
<div className="app">
{showingDetails && <Details nodes={this.state.nodes}
controlError={this.state.controlError}
controlPending={this.state.controlPending}
nodeId={this.state.selectedNodeId}
details={this.state.nodeDetails} /> }

{this.state.controlPipe && <Terminal controlPipe={this.state.controlPipe} />}
{this.state.controlPipes.toKeyedSeq().map((nodeId, pipeId) => {
return (
<Terminal pipeId={pipeId} nodeId={nodeId} nodes={this.state.nodes}
selectedNodeId={this.state.selectedNodeId} />
);
})}

<div className="header">
<Logo />
Expand Down
4 changes: 1 addition & 3 deletions client/app/scripts/components/details.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ const Details = React.createClass({
render: function() {
return (
<div id="details">
<div style={{height: '100%', paddingBottom: 8, borderRadius: 2,
backgroundColor: '#fff',
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.19), 0 6px 10px rgba(0, 0, 0, 0.23)'}}>
<div className="details-wrapper">
<NodeDetails {...this.props} />
</div>
</div>
Expand Down
156 changes: 138 additions & 18 deletions client/app/scripts/components/terminal.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,64 @@
const React = require('react');
const ReactDOM = require('react-dom');
const PureRenderMixin = require('react-addons-pure-render-mixin');
const Draggable = require('react-draggable');
const DraggableCore = Draggable.DraggableCore;

const AppActions = require('../actions/app-actions');
const NodeColorMixin = require('../mixins/node-color-mixin');
const Term = require('../vendor/term.js');

const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = __WS_URL__ || wsProto + '://' + location.host + location.pathname.replace(/\/$/, '');

const DEFAULT_COLS = 80;
const DEFAULT_ROWS = 24;
const MIN_COLS = 40;
const MIN_ROWS = 4;

function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}

const Terminal = React.createClass({

mixins: [
NodeColorMixin,
PureRenderMixin
],

getInitialState: function() {
return {
conected: false,
rows: DEFAULT_ROWS,
cols: DEFAULT_COLS,
pixelPerCol: 0,
pixelPerRow: 0,
height: 0,
width: 0
};
},

componentDidMount: function() {
let socket = new WebSocket(wsUrl + '/api/pipe/' + this.props.controlPipe);
let socket = new WebSocket(wsUrl + '/api/pipe/' + this.props.pipeId);
socket.binaryType = 'arraybuffer';
const component = this;

const term = new Term({
cols: 80,
rows: 24,
cols: this.state.cols,
rows: this.state.rows,
screenKeys: true
});

const innerNode = ReactDOM.findDOMNode(component.inner);
term.open(innerNode);

term.on('data', function(data) {
socket.send(data);
});

socket.onopen = function() {
term.open(component.inner.getDOMNode());
term.write('\x1b[31mWelcome to term.js!\x1b[m\r\n');
console.log('socket open');
};

socket.onclose = function() {
Expand All @@ -37,29 +67,119 @@ const Terminal = React.createClass({
socket = null;
};

socket.onerror = function() {
console.error('socket error');
socket.onerror = function(err) {
console.error('socket error', err);
};

socket.onmessage = function(event) {
console.log('pipe data', event.data.size);
term.write(ab2str(event.data));
const input = ab2str(event.data);
term.write(input);
};

this.term = term;
this.socket = socket;

setTimeout(() => {
this.setState({connected: true});
}, 10);
},

componentWillUnmount: function() {
console.log('cwu terminal');
if (this.socket) {
console.log('destroy terminal');
this.socket.close();
}
this.term = null;
this.socket = null;
},

componentDidUpdate: function() {
console.log('cdu terminal');
this.term.resize(this.state.cols, this.state.rows);
},

handleCloseClick: function(ev) {
ev.preventDefault();
AppActions.clickCloseTerminal(this.props.pipeId);
},

getNode: function() {
// FIXME use consistent node ID that does not include prefix 'hostname;'
const nodeId = this.props.nodeId && this.props.nodeId.split(';').pop();
return this.props.nodes.get(nodeId);
},

handleResizeStart: function() {
const wrapperNode = ReactDOM.findDOMNode(this);
const width = wrapperNode.clientWidth;
const height = wrapperNode.clientHeight;
const pixelPerRow = height / this.state.rows;
const pixelPerCol = width / this.state.cols;
this.setState({width, height, pixelPerCol, pixelPerRow});
},

handleResize: function(e, {position}) {
const width = this.state.width + position.deltaX;
const height = this.state.height + position.deltaY;
// calculate cols and rows based on new dimensions
const cols = Math.max(MIN_COLS, Math.floor(width / this.state.pixelPerCol));
const rows = Math.max(MIN_ROWS, Math.floor(height / this.state.pixelPerRow));
this.setState({width, height, cols, rows});
},

handleResizeStop: function() {
const width = this.state.cols * this.state.pixelPerCol;
const height = this.state.rows * this.state.pixelPerRow;
this.setState({width, height});
},

handleClick: function() {
AppActions.clickTerminal(this.props.pipeId);
},

render: function() {
const node = this.getNode();

if (!this.props.pipeId) {
return <div/>; // TODO this.renderLoading();
}

const nodeColor = node ? this.getNodeColorDark(node.get('label_major')) : this.getNeutralColor();
const title = node ? node.get('label_major') : 'n/a';
const styles = {
header: {
backgroundColor: node ? nodeColor : 'red'
},
wrapper: {
opacity: node && this.state.connected ? 1 : 0.6,
left: 16
}
};

return (
<div id="terminal">
<div style={{height: '100%', paddingBottom: 8, borderRadius: 2,
backgroundColor: '#fff',
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.19), 0 6px 10px rgba(0, 0, 0, 0.23)',
fontFamily: '"DejaVu Sans Mono", "Liberation Mono", monospace',
color: '#f0f0f0'
}}
ref={(ref) => this.inner = ref}>
Pipe: {this.props.controlPipe}
<Draggable handle=".terminal-header" bounds="parent">
<div className="terminal-wrapper hideable" style={styles.wrapper} onClick={this.handleClick}>
<div className="terminal-header" style={styles.header}>
<div className="terminal-header-tools">
<span className="terminal-header-tools-icon fa fa-close" onClick={this.handleCloseClick} />
</div>
<span className="terminal-header-title">
Terminal {title} &mdash; {this.state.cols}&times;{this.state.rows}
</span>
</div>
<div className="terminal-inner" ref={(ref) => this.inner = ref} />
<DraggableCore
onStop={this.handleResizeStop}
onStart={this.handleResizeStart}
onDrag={this.handleResize}>
<div className="terminal-footer">
<span className="terminal-resize-handle fa fa-sort" />
</div>
</DraggableCore>
</div>
</div>
</Draggable>
);
}

Expand Down
2 changes: 2 additions & 0 deletions client/app/scripts/constants/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module.exports = keymirror({
CHANGE_TOPOLOGY_OPTION: null,
CLEAR_CONTROL_ERROR: null,
CLICK_CLOSE_DETAILS: null,
CLICK_CLOSE_TERMINAL: null,
CLICK_TERMINAL: null,
CLICK_NODE: null,
CLICK_TOPOLOGY: null,
CLOSE_WEBSOCKET: null,
Expand Down
2 changes: 1 addition & 1 deletion client/app/scripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ const App = require('./components/app.js');

ReactDOM.render(
<App/>,
document.getElementById('app'));
document.body);
4 changes: 4 additions & 0 deletions client/app/scripts/mixins/node-color-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ colors(internetLabel);


const NodeColorMixin = {
getNeutralColor: function() {
return '#b1b1cb';
},

getNodeColor: function(text) {
return colors(text);
},
Expand Down
Loading

0 comments on commit 9d72ee8

Please sign in to comment.