mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 01:31:17 +00:00
Terminal UI for pipes
- term.js - Add eslintignore - Fix color and es2015 after rebase - Fix JS test, probably deleted during conflict resolution - Moves terminal close button to top-right of window - Consitent w/ details window. - Changes padding of details window close button so both close buttons are horizonally aligned. - Terminal resizes w/ browser window. - No longer can drag window around. - Add tiny big of padding between term and node-details. - Playing w/ terminal placement. This one's more drawer-like. - Send DELETE when we close a terminal window. - Dont lint or bable JS vendor files - Ignore ctags 'tags' files. - Adds popping out terminal out into a new browser window. - Simplify code as now we've just a single terminal window. - ESC is back to close the terminal, then the details panel. - Fixes bug w/ slow response to closing the details panel. - Moving away from "drawer-style" for terminal size and position to a simple window. - Just gotta handle the case for refreshing a popped out terminal. - Stop terminal text being auto-deselected. - window resizes will still deselect. - Adds state.connected to react.scu check. - Don't delete pipe when browser closes - To allow for nicer refresh flows - scope-app will time out pipe after a while. - Keep terminal-open/closed state in the url. - shouldComponentUpdate fix to prevent deselection of text has been rolled back so gotta come up w/ another way to handle that... - Fixes terminal text-selection again. - Make pipes work for non-raw terminals too. - Move document.title updating somewhere more sensible. - Pass rawTty prop along to all terminals. - Don't render react root into doc.body - Reconnect the websocket if we lose it. - First, slightly rough, attempt at displaying if pipe has been deleted - Refactor controlPipe structure in the AppStore/hash. - Merge controlPipeId, controlPipeRaw, controlPipeStatus into a single object. - Adds a status bar to the terminal window. - Error handling in popout working again. - Don't show terminal cursor when not connected. - Simplify controlPipe status and error handling. - Don't keep the status in the hash. - Use special new action receiveControlPipeFromParams rather than adding lots of branching to receiveControlPipe. - You can reload a terminal but it doesn't exist in history stack. - Pull out terminal into its own entry point! - Fixes prod webpack build - Fixes terminal-app websocket path when running on prod. - Fixes old terminals appearing when closing a terminal. - History hacking wasn't working, this is a little simpler.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,6 +30,9 @@ coverage.html
|
||||
# Emacs backup files
|
||||
*~
|
||||
|
||||
# ctags files
|
||||
tags
|
||||
|
||||
# Project specific
|
||||
.*.uptodate
|
||||
scope.tar
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
app/scripts/vendor/term.js
|
||||
|
||||
@@ -4,4 +4,4 @@ COPY package.json /home/weave/
|
||||
ENV NPM_CONFIG_LOGLEVEL warn
|
||||
# Dont install optional developer tools
|
||||
RUN npm install --no-optional
|
||||
COPY webpack.local.config.js webpack.production.config.js server.js .babelrc .eslintrc /home/weave/
|
||||
COPY webpack.local.config.js webpack.production.config.js server.js .babelrc .eslintrc .eslintignore /home/weave/
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import AppDispatcher from '../dispatcher/app-dispatcher';
|
||||
import ActionTypes from '../constants/action-types';
|
||||
|
||||
import { updateRoute } from '../utils/router-utils';
|
||||
import { doControl as doControlRequest, getNodesDelta, getNodeDetails, getTopologies } from '../utils/web-api-utils';
|
||||
import { doControl as doControlRequest, getNodesDelta, getNodeDetails,
|
||||
getTopologies, deletePipe } from '../utils/web-api-utils';
|
||||
import AppStore from '../stores/app-store';
|
||||
|
||||
const log = debug('scope:app-actions');
|
||||
|
||||
export function changeTopologyOption(option, value, topologyId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
|
||||
@@ -34,6 +38,17 @@ export function clickCloseDetails() {
|
||||
updateRoute();
|
||||
}
|
||||
|
||||
export function clickCloseTerminal(pipeId, closePipe) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_CLOSE_TERMINAL,
|
||||
pipeId: pipeId
|
||||
});
|
||||
if (closePipe) {
|
||||
deletePipe(pipeId);
|
||||
}
|
||||
updateRoute();
|
||||
}
|
||||
|
||||
export function clickNode(nodeId) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_NODE,
|
||||
@@ -102,10 +117,18 @@ export function enterNode(nodeId) {
|
||||
}
|
||||
|
||||
export function hitEsc() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.HIT_ESC_KEY
|
||||
});
|
||||
updateRoute();
|
||||
//
|
||||
// This simulates an "ESC-stack"
|
||||
// 1st esc removes the controlPipe and closes the terminal.
|
||||
// 2nd esc deselects the node and closes the details.
|
||||
//
|
||||
const controlPipe = AppStore.getControlPipe();
|
||||
if (controlPipe) {
|
||||
clickCloseTerminal(controlPipe.id, true);
|
||||
} else {
|
||||
AppDispatcher.dispatch({type: ActionTypes.DESELECT_NODE});
|
||||
updateRoute();
|
||||
}
|
||||
}
|
||||
|
||||
export function leaveEdge(edgeId) {
|
||||
@@ -172,6 +195,43 @@ export function receiveApiDetails(apiDetails) {
|
||||
});
|
||||
}
|
||||
|
||||
export function receiveControlPipeFromParams(pipeId, rawTty) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_CONTROL_PIPE,
|
||||
pipeId: pipeId,
|
||||
rawTty: rawTty
|
||||
});
|
||||
}
|
||||
|
||||
export function receiveControlPipe(pipeId, nodeId, rawTty) {
|
||||
if (nodeId.split(';').pop() !== AppStore.getSelectedNodeId()) {
|
||||
log('Node was deselected before we could set up control!');
|
||||
deletePipe(pipeId);
|
||||
return;
|
||||
}
|
||||
|
||||
const controlPipe = AppStore.getControlPipe();
|
||||
if (controlPipe && controlPipe.id !== pipeId) {
|
||||
deletePipe(controlPipe.id);
|
||||
}
|
||||
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_CONTROL_PIPE,
|
||||
pipeId: pipeId,
|
||||
rawTty: rawTty
|
||||
});
|
||||
|
||||
updateRoute();
|
||||
}
|
||||
|
||||
export function receiveControlPipeStatus(pipeId, status) {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.RECEIVE_CONTROL_PIPE_STATUS,
|
||||
pipeId: pipeId,
|
||||
status: status
|
||||
});
|
||||
}
|
||||
|
||||
export function receiveError(errorUrl) {
|
||||
AppDispatcher.dispatch({
|
||||
errorUrl: errorUrl,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getApiDetails, getTopologies } from '../utils/web-api-utils';
|
||||
import { hitEsc } from '../actions/app-actions';
|
||||
import Details from './details';
|
||||
import Nodes from './nodes';
|
||||
import EmbeddedTerminal from './embedded-terminal';
|
||||
import { getRouter } from '../utils/router-utils';
|
||||
|
||||
const ESC_KEY_CODE = 27;
|
||||
@@ -19,6 +20,7 @@ function getStateFromStores() {
|
||||
activeTopologyOptions: AppStore.getActiveTopologyOptions(),
|
||||
controlError: AppStore.getControlError(),
|
||||
controlPending: AppStore.isControlPending(),
|
||||
controlPipe: AppStore.getControlPipe(),
|
||||
currentTopology: AppStore.getCurrentTopology(),
|
||||
currentTopologyId: AppStore.getCurrentTopologyId(),
|
||||
currentTopologyOptions: AppStore.getCurrentTopologyOptions(),
|
||||
@@ -33,11 +35,12 @@ function getStateFromStores() {
|
||||
topologiesLoaded: AppStore.isTopologiesLoaded(),
|
||||
version: AppStore.getVersion(),
|
||||
websocketClosed: AppStore.isWebsocketClosed()
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default class App extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
@@ -69,18 +72,24 @@ export default class App extends React.Component {
|
||||
|
||||
render() {
|
||||
const showingDetails = this.state.selectedNodeId;
|
||||
const showingTerminal = this.state.controlPipe;
|
||||
const footer = `Version ${this.state.version} on ${this.state.hostname}`;
|
||||
// width of details panel blocking a view
|
||||
const detailsWidth = showingDetails ? 450 : 0;
|
||||
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} /> }
|
||||
details={this.state.nodeDetails} />}
|
||||
|
||||
{showingTerminal && <EmbeddedTerminal
|
||||
pipe={this.state.controlPipe}
|
||||
nodeId={this.state.selectedNodeId}
|
||||
nodes={this.state.nodes} />}
|
||||
|
||||
<div className="header">
|
||||
<Logo />
|
||||
|
||||
@@ -17,9 +17,7 @@ export default class Details extends React.Component {
|
||||
render() {
|
||||
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">
|
||||
<div className="details-tools-wrapper">
|
||||
<div className="details-tools">
|
||||
<span className="fa fa-close" onClick={this.handleClickClose} />
|
||||
|
||||
20
client/app/scripts/components/embedded-terminal.js
Normal file
20
client/app/scripts/components/embedded-terminal.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getNodeColor, getNodeColorDark } from '../utils/color-utils';
|
||||
import Terminal from './terminal';
|
||||
|
||||
export default function EmeddedTerminal({pipe, nodeId, nodes}) {
|
||||
const node = nodes.get(nodeId && nodeId.split(';').pop());
|
||||
const titleBarColor = node && getNodeColorDark(node.get('rank'), node.get('label_major'));
|
||||
const statusBarColor = node && getNodeColor(node.get('rank'), node.get('label_major'));
|
||||
const title = node && node.get('label_major');
|
||||
|
||||
// React unmount/remounts when key changes, this is important for cleaning up
|
||||
// the term.js and creating a new one for the new pipe.
|
||||
return (
|
||||
<div className="terminal-embedded">
|
||||
<Terminal key={pipe.id} pipe={pipe} titleBarColor={titleBarColor}
|
||||
statusBarColor={statusBarColor} title={title} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
client/app/scripts/components/terminal-app.js
Normal file
57
client/app/scripts/components/terminal-app.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
|
||||
import AppStore from '../stores/app-store';
|
||||
import Terminal from './terminal';
|
||||
import { receiveControlPipeFromParams } from '../actions/app-actions';
|
||||
|
||||
export function shouldLaunchTerminal() {
|
||||
return window.location.hash.indexOf('#!/terminal/') === 0;
|
||||
}
|
||||
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
controlPipe: AppStore.getControlPipe()
|
||||
};
|
||||
}
|
||||
|
||||
export class TerminalApp extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
const paramString = window.location.hash.split('/').pop();
|
||||
const params = JSON.parse(paramString);
|
||||
receiveControlPipeFromParams(params.pipe.id, null, params.pipe.raw, false);
|
||||
|
||||
this.state = {
|
||||
titleBarColor: params.titleBarColor,
|
||||
statusBarColor: params.statusBarColor,
|
||||
controlPipe: AppStore.getControlPipe()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AppStore.addListener(this.onChange);
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState(getStateFromStores());
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {borderTop: `4px solid ${this.state.titleBarColor}`};
|
||||
|
||||
return (
|
||||
<div className="terminal-app" style={style}>
|
||||
{this.state.controlPipe && <Terminal
|
||||
key={this.state.controlPipe.id}
|
||||
pipe={this.state.controlPipe}
|
||||
titleBarColor={this.state.titleBarColor}
|
||||
statusBarColor={this.state.statusBarColor}
|
||||
title={this.state.title}
|
||||
embedded={false} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
283
client/app/scripts/components/terminal.js
Normal file
283
client/app/scripts/components/terminal.js
Normal file
@@ -0,0 +1,283 @@
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { clickCloseTerminal } from '../actions/app-actions';
|
||||
import { getNeutralColor } from '../utils/color-utils';
|
||||
import { setDocumentTitle } from '../utils/title-utils';
|
||||
import { getPipeStatus, basePath } from '../utils/web-api-utils';
|
||||
import Term from '../vendor/term.js';
|
||||
|
||||
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const wsUrl = __WS_URL__ || wsProto + '://' + location.host + basePath(location.pathname);
|
||||
const log = debug('scope:terminal');
|
||||
|
||||
const DEFAULT_COLS = 80;
|
||||
const DEFAULT_ROWS = 24;
|
||||
const MIN_COLS = 40;
|
||||
const MIN_ROWS = 4;
|
||||
// Unicode points can be used in html and document.title
|
||||
// html shorthand codes (×) don't work in document.title.
|
||||
const TIMES = '\u00D7';
|
||||
const MDASH = '\u2014';
|
||||
|
||||
const reconnectTimerInterval = 2000;
|
||||
let reconnectTimer = null;
|
||||
|
||||
function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buf));
|
||||
}
|
||||
|
||||
function terminalCellSize(wrapperNode, rows, cols) {
|
||||
//
|
||||
// wrapperNode should be an unsized block. E.g. An element that 'clings' to
|
||||
// its inner terminal so we get a nice size reading of the inner terminal.
|
||||
//
|
||||
const width = wrapperNode.clientWidth;
|
||||
const height = wrapperNode.clientHeight;
|
||||
const pixelPerRow = height / rows;
|
||||
const pixelPerCol = width / cols;
|
||||
return {pixelPerCol, pixelPerRow};
|
||||
}
|
||||
|
||||
function openNewWindow(url) {
|
||||
const screenLeft = window.screenX || window.screenLeft;
|
||||
const screenTop = window.screenY || window.screenTop;
|
||||
const popoutWindowToolbarHeight = 51;
|
||||
const windowOptions = {
|
||||
width: window.innerWidth - 420 - 36 - 36 - 4,
|
||||
height: window.innerHeight - 24 - 48 - popoutWindowToolbarHeight,
|
||||
left: screenLeft + 36,
|
||||
top: screenTop + (window.outerHeight - window.innerHeight) + 24,
|
||||
location: 'no',
|
||||
};
|
||||
|
||||
const windowOptionsString = Object.keys(windowOptions)
|
||||
.map((k) => k + '=' + windowOptions[k])
|
||||
.join(',');
|
||||
|
||||
window.open(url, '', windowOptionsString);
|
||||
}
|
||||
|
||||
export default class Terminal extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
connected: false,
|
||||
rows: DEFAULT_ROWS,
|
||||
cols: DEFAULT_COLS,
|
||||
pixelPerCol: 0,
|
||||
pixelPerRow: 0
|
||||
};
|
||||
this.handleCloseClick = this.handleCloseClick.bind(this);
|
||||
this.handlePopoutTerminal = this.handlePopoutTerminal.bind(this);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
}
|
||||
|
||||
createWebsocket(term) {
|
||||
const socket = new WebSocket(wsUrl + '/api/pipe/' + this.getPipeId());
|
||||
socket.binaryType = 'arraybuffer';
|
||||
|
||||
getPipeStatus(this.getPipeId());
|
||||
|
||||
socket.onopen = () => {
|
||||
clearTimeout(reconnectTimer);
|
||||
log('socket open to', wsUrl);
|
||||
this.setState({connected: true});
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
log('socket closed');
|
||||
this.setState({connected: false});
|
||||
this.socket = null;
|
||||
if (this.term && this.props.pipe.status !== 'PIPE_DELETED') {
|
||||
reconnectTimer = setTimeout(
|
||||
this.createWebsocket.bind(this, term), reconnectTimerInterval);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = (err) => {
|
||||
log('socket error', err);
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
log('pipe data', event.data.size);
|
||||
const input = ab2str(event.data);
|
||||
term.write(input);
|
||||
};
|
||||
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const component = this;
|
||||
|
||||
this.term = new Term({
|
||||
cols: this.state.cols,
|
||||
rows: this.state.rows,
|
||||
screenKeys: true,
|
||||
convertEol: !this.props.raw
|
||||
});
|
||||
|
||||
const innerNode = ReactDOM.findDOMNode(component.inner);
|
||||
this.term.open(innerNode);
|
||||
this.term.on('data', (data) => {
|
||||
if (this.socket) {
|
||||
this.socket.send(data);
|
||||
}
|
||||
});
|
||||
|
||||
this.createWebsocket(this.term);
|
||||
|
||||
const {pixelPerCol, pixelPerRow} = terminalCellSize(
|
||||
innerNode, this.state.rows, this.state.cols);
|
||||
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
pixelPerCol: pixelPerCol,
|
||||
pixelPerRow: pixelPerRow
|
||||
});
|
||||
this.handleResize();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
log('cwu terminal');
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
if (this.term) {
|
||||
log('destroy terminal');
|
||||
this.term.destroy();
|
||||
this.term = null;
|
||||
}
|
||||
if (this.socket) {
|
||||
log('close socket');
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
log('cdu terminal');
|
||||
const sizeChanged = (
|
||||
prevState.cols !== this.state.cols ||
|
||||
prevState.rows !== this.state.rows
|
||||
);
|
||||
if (sizeChanged) {
|
||||
this.term.resize(this.state.cols, this.state.rows);
|
||||
}
|
||||
if (!this.props.embedded) {
|
||||
setDocumentTitle(this.getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
handleCloseClick(ev) {
|
||||
ev.preventDefault();
|
||||
clickCloseTerminal(this.getPipeId(), true);
|
||||
}
|
||||
|
||||
handlePopoutTerminal(ev) {
|
||||
ev.preventDefault();
|
||||
const paramString = JSON.stringify(this.props);
|
||||
clickCloseTerminal(this.getPipeId());
|
||||
openNewWindow(`/terminal.html#!/state/${paramString}`);
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
const innerNode = ReactDOM.findDOMNode(this.innerFlex);
|
||||
const width = innerNode.clientWidth - (2 * 8);
|
||||
const height = innerNode.clientHeight - (2 * 8);
|
||||
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({cols, rows});
|
||||
}
|
||||
|
||||
isEmbedded() {
|
||||
return (this.props.embedded !== false);
|
||||
}
|
||||
|
||||
getPipeId() {
|
||||
return this.props.pipe.id;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
const nodeName = this.props.title || 'n/a';
|
||||
return `Terminal ${nodeName} ${MDASH}
|
||||
${this.state.cols}${TIMES}${this.state.rows}`;
|
||||
}
|
||||
|
||||
getTerminalHeader() {
|
||||
const style = {
|
||||
backgroundColor: this.props.titleBarColor || getNeutralColor()
|
||||
};
|
||||
return (
|
||||
<div className="terminal-header" style={style}>
|
||||
<div className="terminal-header-tools">
|
||||
<span className="terminal-header-tools-icon fa fa-external-link"
|
||||
onClick={this.handlePopoutTerminal} />
|
||||
<span className="terminal-header-tools-icon fa fa-close"
|
||||
onClick={this.handleCloseClick} />
|
||||
</div>
|
||||
<span className="terminal-header-title">{this.getTitle()}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
if (this.props.pipe.status === 'PIPE_DELETED') {
|
||||
return {
|
||||
title: 'Pipe Deleted.',
|
||||
message: 'The control pipe for the connection to this container has ' +
|
||||
'been deleted. Try performing the attach or exec again.'
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.state.connected) {
|
||||
return {
|
||||
title: 'Connecting...',
|
||||
message: 'Establishing a connection to the container.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Connected',
|
||||
message: 'Lets do this!'
|
||||
};
|
||||
}
|
||||
|
||||
getTerminalStatusBar() {
|
||||
const {title, message} = this.getStatus();
|
||||
const style = {
|
||||
backgroundColor: this.props.statusBarColor || getNeutralColor(),
|
||||
opacity: this.state.connected ? 0 : 0.9
|
||||
};
|
||||
return (
|
||||
<div className="terminal-status-bar hideable" style={style}>
|
||||
<h1>{title}</h1>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const innerStyle = {
|
||||
opacity: this.state.connected ? 1 : 0.8
|
||||
};
|
||||
const innerClassName = classNames('terminal-inner hideable', {
|
||||
'terminal-inactive': !this.state.connected
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="terminal-wrapper">
|
||||
{this.isEmbedded() && this.getTerminalHeader()}
|
||||
<div ref={(ref) => this.innerFlex = ref}
|
||||
className={innerClassName} style={innerStyle} >
|
||||
<div ref={(ref) => this.inner = ref} />
|
||||
</div>
|
||||
{this.getTerminalStatusBar()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,22 @@ const ACTION_TYPES = [
|
||||
'CHANGE_TOPOLOGY_OPTION',
|
||||
'CLEAR_CONTROL_ERROR',
|
||||
'CLICK_CLOSE_DETAILS',
|
||||
'CLICK_CLOSE_TERMINAL',
|
||||
'CLICK_NODE',
|
||||
'CLICK_TERMINAL',
|
||||
'CLICK_TOPOLOGY',
|
||||
'CLOSE_WEBSOCKET',
|
||||
'DESELECT_NODE',
|
||||
'DO_CONTROL',
|
||||
'DO_CONTROL_ERROR',
|
||||
'DO_CONTROL_SUCCESS',
|
||||
'ENTER_EDGE',
|
||||
'ENTER_NODE',
|
||||
'HIT_ESC_KEY',
|
||||
'LEAVE_EDGE',
|
||||
'LEAVE_NODE',
|
||||
'OPEN_WEBSOCKET',
|
||||
'RECEIVE_CONTROL_PIPE',
|
||||
'RECEIVE_CONTROL_PIPE_STATUS',
|
||||
'RECEIVE_NODE_DETAILS',
|
||||
'RECEIVE_NODES',
|
||||
'RECEIVE_NODES_DELTA',
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Dispatcher } from 'flux';
|
||||
import _ from 'lodash';
|
||||
import debug from 'debug';
|
||||
const log = debug('scope:dispatcher');
|
||||
|
||||
const instance = new Dispatcher();
|
||||
|
||||
instance.dispatch = _.wrap(Dispatcher.prototype.dispatch, function(func) {
|
||||
const args = Array.prototype.slice.call(arguments, 1);
|
||||
// console.log(args[0]);
|
||||
const type = args[0] && args[0].type;
|
||||
log(type, args[0]);
|
||||
func.apply(this, args);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,4 @@ import ReactDOM from 'react-dom';
|
||||
|
||||
import App from './components/app.js';
|
||||
|
||||
ReactDOM.render(
|
||||
<App/>,
|
||||
document.getElementById('app'));
|
||||
ReactDOM.render(<App/>, document.getElementById('app'));
|
||||
|
||||
@@ -70,8 +70,8 @@ describe('AppStore', function() {
|
||||
type: ActionTypes.CLOSE_WEBSOCKET
|
||||
};
|
||||
|
||||
const HitEscAction = {
|
||||
type: ActionTypes.HIT_ESC_KEY
|
||||
const deSelectNode = {
|
||||
type: ActionTypes.DESELECT_NODE
|
||||
};
|
||||
|
||||
const OpenWebsocketAction = {
|
||||
@@ -251,7 +251,7 @@ describe('AppStore', function() {
|
||||
expect(AppStore.getSelectedNodeId()).toBe('n1');
|
||||
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
|
||||
|
||||
registeredCallback(HitEscAction);
|
||||
registeredCallback(deSelectNode);
|
||||
expect(AppStore.getSelectedNodeId()).toBe(null);
|
||||
expect(AppStore.getNodes().toJS()).toEqual(NODE_SET);
|
||||
});
|
||||
@@ -318,7 +318,7 @@ describe('AppStore', function() {
|
||||
expect(AppStore.getAdjacentNodes('n1').has('n1')).toBeTruthy();
|
||||
expect(AppStore.getAdjacentNodes('n1').has('n2')).toBeTruthy();
|
||||
|
||||
registeredCallback(HitEscAction);
|
||||
registeredCallback(deSelectNode);
|
||||
expect(AppStore.getAdjacentNodes().size).toEqual(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ const makeOrderedMap = Immutable.OrderedMap;
|
||||
const makeSet = Immutable.Set;
|
||||
const log = debug('scope:app-store');
|
||||
|
||||
const error = debug('scope:error');
|
||||
|
||||
// Helpers
|
||||
|
||||
function findCurrentTopology(subTree, topologyId) {
|
||||
@@ -44,7 +46,7 @@ function makeNode(node) {
|
||||
|
||||
// Initial values
|
||||
|
||||
let topologyOptions = makeOrderedMap();
|
||||
let topologyOptions = makeOrderedMap(); // topologyId -> options
|
||||
let adjacentNodes = makeSet();
|
||||
let controlError = null;
|
||||
let controlPending = false;
|
||||
@@ -55,12 +57,13 @@ let hostname = '...';
|
||||
let version = '...';
|
||||
let mouseOverEdgeId = null;
|
||||
let mouseOverNodeId = null;
|
||||
let nodes = makeOrderedMap();
|
||||
let nodes = makeOrderedMap(); // nodeId -> node
|
||||
let nodeDetails = null;
|
||||
let selectedNodeId = null;
|
||||
let topologies = [];
|
||||
let topologiesLoaded = false;
|
||||
let routeSet = false;
|
||||
let controlPipe = null;
|
||||
let websocketClosed = true;
|
||||
|
||||
function processTopologies(topologyList) {
|
||||
@@ -102,6 +105,7 @@ function setDefaultTopologyOptions(topologyList) {
|
||||
function deSelectNode() {
|
||||
selectedNodeId = null;
|
||||
nodeDetails = null;
|
||||
controlPipe = null;
|
||||
}
|
||||
|
||||
// Store API
|
||||
@@ -113,6 +117,7 @@ export class AppStore extends Store {
|
||||
return {
|
||||
topologyId: currentTopologyId,
|
||||
selectedNodeId: this.getSelectedNodeId(),
|
||||
controlPipe: this.getControlPipe(),
|
||||
topologyOptions: topologyOptions.toJS() // all options
|
||||
};
|
||||
}
|
||||
@@ -142,6 +147,10 @@ export class AppStore extends Store {
|
||||
return controlError;
|
||||
}
|
||||
|
||||
getControlPipe() {
|
||||
return controlPipe;
|
||||
}
|
||||
|
||||
getCurrentTopology() {
|
||||
if (!currentTopology) {
|
||||
currentTopology = setTopology(currentTopologyId);
|
||||
@@ -244,8 +253,11 @@ export class AppStore extends Store {
|
||||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
switch (payload.type) {
|
||||
if (!payload.type) {
|
||||
error('Payload missing a type!', payload);
|
||||
}
|
||||
|
||||
switch (payload.type) {
|
||||
case ActionTypes.CHANGE_TOPOLOGY_OPTION:
|
||||
if (topologyOptions.getIn([payload.topologyId, payload.option])
|
||||
!== payload.value) {
|
||||
@@ -268,6 +280,11 @@ export class AppStore extends Store {
|
||||
this.__emitChange();
|
||||
break;
|
||||
|
||||
case ActionTypes.CLICK_CLOSE_TERMINAL:
|
||||
controlPipe = null;
|
||||
this.__emitChange();
|
||||
break;
|
||||
|
||||
case ActionTypes.CLICK_NODE:
|
||||
deSelectNode();
|
||||
if (payload.nodeId !== selectedNodeId) {
|
||||
@@ -307,7 +324,7 @@ export class AppStore extends Store {
|
||||
this.__emitChange();
|
||||
break;
|
||||
|
||||
case ActionTypes.HIT_ESC_KEY:
|
||||
case ActionTypes.DESELECT_NODE:
|
||||
deSelectNode();
|
||||
this.__emitChange();
|
||||
break;
|
||||
@@ -342,6 +359,21 @@ export class AppStore extends Store {
|
||||
this.__emitChange();
|
||||
break;
|
||||
|
||||
case ActionTypes.RECEIVE_CONTROL_PIPE:
|
||||
controlPipe = {
|
||||
id: payload.pipeId,
|
||||
raw: payload.rawTty
|
||||
};
|
||||
this.__emitChange();
|
||||
break;
|
||||
|
||||
case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS:
|
||||
if (controlPipe) {
|
||||
controlPipe.status = payload.status;
|
||||
this.__emitChange();
|
||||
}
|
||||
break;
|
||||
|
||||
case ActionTypes.RECEIVE_ERROR:
|
||||
errorUrl = payload.errorUrl;
|
||||
this.__emitChange();
|
||||
@@ -421,6 +453,7 @@ export class AppStore extends Store {
|
||||
setTopology(payload.state.topologyId);
|
||||
setDefaultTopologyOptions(topologies);
|
||||
selectedNodeId = payload.state.selectedNodeId;
|
||||
controlPipe = payload.state.controlPipe;
|
||||
topologyOptions = Immutable.fromJS(payload.state.topologyOptions)
|
||||
|| topologyOptions;
|
||||
this.__emitChange();
|
||||
|
||||
9
client/app/scripts/terminal-main.js
Normal file
9
client/app/scripts/terminal-main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
require('font-awesome-webpack');
|
||||
require('../styles/main.less');
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { TerminalApp } from './components/terminal-app.js';
|
||||
|
||||
ReactDOM.render(<TerminalApp/>, document.getElementById('app'));
|
||||
25
client/app/scripts/utils/__tests__/web-api-utils-test.js
Normal file
25
client/app/scripts/utils/__tests__/web-api-utils-test.js
Normal file
@@ -0,0 +1,25 @@
|
||||
jest.dontMock('../web-api-utils');
|
||||
|
||||
describe('WebApiUtils', function() {
|
||||
const WebApiUtils = require('../web-api-utils');
|
||||
|
||||
describe('basePath', function() {
|
||||
const basePath = WebApiUtils.basePath;
|
||||
|
||||
it('should handle /scope/terminal.html', function() {
|
||||
expect(basePath("/scope/terminal.html")).toBe("/scope");
|
||||
});
|
||||
|
||||
it('should handle /scope/', function() {
|
||||
expect(basePath("/scope/")).toBe("/scope");
|
||||
});
|
||||
|
||||
it('should handle /scope', function() {
|
||||
expect(basePath("/scope")).toBe("/scope");
|
||||
});
|
||||
|
||||
it('should handle /', function() {
|
||||
expect(basePath("/")).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -40,8 +40,12 @@ function colors(text, secondText) {
|
||||
return color;
|
||||
}
|
||||
|
||||
export function getNeutralColor() {
|
||||
return PSEUDO_COLOR;
|
||||
}
|
||||
|
||||
export function getNodeColor(text, secondText) {
|
||||
return colors(text, secondText);
|
||||
return colors(text, secondText).toString();
|
||||
}
|
||||
|
||||
export function getNodeColorDark(text, secondText) {
|
||||
|
||||
@@ -3,12 +3,31 @@ import page from 'page';
|
||||
import { route } from '../actions/app-actions';
|
||||
import AppStore from '../stores/app-store';
|
||||
|
||||
//
|
||||
// TODO: move this logic somewhere else.
|
||||
//
|
||||
function shouldReplaceState(prevState, nextState) {
|
||||
return (
|
||||
// Opening a new terminal while an existing one is open.
|
||||
(prevState.controlPipe && nextState.controlPipe) ||
|
||||
// Closing a terminal.
|
||||
(prevState.controlPipe && !nextState.controlPipe)
|
||||
);
|
||||
}
|
||||
|
||||
export function updateRoute() {
|
||||
const state = AppStore.getAppState();
|
||||
const stateUrl = JSON.stringify(state);
|
||||
const dispatch = false;
|
||||
const urlStateString = window.location.hash.replace('#!/state/', '') || '{}';
|
||||
const prevState = JSON.parse(urlStateString);
|
||||
|
||||
page.show('/state/' + stateUrl, state, dispatch);
|
||||
if (shouldReplaceState(prevState, state)) {
|
||||
// Replace the top of the history rather than pushing on a new item.
|
||||
page.replace('/state/' + stateUrl, state, dispatch);
|
||||
} else {
|
||||
page.show('/state/' + stateUrl, state, dispatch);
|
||||
}
|
||||
}
|
||||
|
||||
page('/', function() {
|
||||
|
||||
@@ -3,7 +3,8 @@ import reqwest from 'reqwest';
|
||||
|
||||
import { clearControlError, closeWebsocket, openWebsocket, receiveError,
|
||||
receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError,
|
||||
receiveControlSuccess, receiveTopologies } from '../actions/app-actions';
|
||||
receiveControlPipe, receiveControlPipeStatus, receiveControlSuccess,
|
||||
receiveTopologies } from '../actions/app-actions';
|
||||
|
||||
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const wsUrl = __WS_URL__ || wsProto + '://' + location.host + location.pathname.replace(/\/$/, '');
|
||||
@@ -31,6 +32,21 @@ function buildOptionsQuery(options) {
|
||||
return '';
|
||||
}
|
||||
|
||||
export function basePath(urlPath) {
|
||||
//
|
||||
// "/scope/terminal.html" -> "/scope"
|
||||
// "/scope/" -> "/scope"
|
||||
// "/scope" -> "/scope"
|
||||
// "/" -> ""
|
||||
//
|
||||
const parts = urlPath.split('/');
|
||||
// if the last item has a "." in it, e.g. foo.html...
|
||||
if (parts[parts.length - 1].indexOf('.') !== -1) {
|
||||
return parts.slice(0, -1).join('/');
|
||||
}
|
||||
return parts.join('/').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function createWebsocket(topologyUrl, optionsQuery) {
|
||||
if (socket) {
|
||||
socket.onclose = null;
|
||||
@@ -146,8 +162,11 @@ export function doControl(probeId, nodeId, control) {
|
||||
reqwest({
|
||||
method: 'POST',
|
||||
url: url,
|
||||
success: function() {
|
||||
success: function(res) {
|
||||
receiveControlSuccess();
|
||||
if (res && res.pipe) {
|
||||
receiveControlPipe(res.pipe, nodeId, res.raw_tty, true);
|
||||
}
|
||||
},
|
||||
error: function(err) {
|
||||
receiveControlError(err.response);
|
||||
@@ -157,3 +176,42 @@ export function doControl(probeId, nodeId, control) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function deletePipe(pipeId) {
|
||||
const url = `api/pipe/${encodeURIComponent(pipeId)}`;
|
||||
reqwest({
|
||||
method: 'DELETE',
|
||||
url: url,
|
||||
success: function() {
|
||||
log('Closed the pipe!');
|
||||
},
|
||||
error: function(err) {
|
||||
log('Error closing pipe:' + err);
|
||||
receiveError(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getPipeStatus(pipeId) {
|
||||
const url = `/api/pipe/${encodeURIComponent(pipeId)}`;
|
||||
reqwest({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
success: function(res) {
|
||||
log('ERROR: expected responses: [400, 404]. Got:', res);
|
||||
},
|
||||
error: function(err) {
|
||||
const status = {
|
||||
400: 'PIPE_ALIVE',
|
||||
404: 'PIPE_DELETED'
|
||||
}[err.status];
|
||||
|
||||
if (!status) {
|
||||
log('Unexpected pipe status:', err.status);
|
||||
return;
|
||||
}
|
||||
|
||||
receiveControlPipeStatus(pipeId, status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
5973
client/app/scripts/vendor/term.js
vendored
Normal file
5973
client/app/scripts/vendor/term.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,9 @@
|
||||
@weave-orange: rgb(255,75,25);
|
||||
@weave-charcoal-blue: rgb(50,50,75); // #32324B
|
||||
|
||||
@base-font: "Roboto", sans-serif;
|
||||
@mono-font: "Menlo", "DejaVu Sans Mono", "Liberation Mono", monospace;
|
||||
|
||||
@primary-color: @weave-charcoal-blue;
|
||||
@background-color: lighten(@primary-color, 66%);
|
||||
@background-secondary-color: lighten(@background-color, 8%);
|
||||
@@ -29,6 +32,9 @@
|
||||
@text-darker-color: @primary-color;
|
||||
@white: @background-secondary-color;
|
||||
|
||||
@details-window-width: 420px;
|
||||
@details-window-padding-left: 36px;
|
||||
|
||||
/* add this class to truncate text with ellipsis, container needs width */
|
||||
.truncate {
|
||||
white-space: nowrap;
|
||||
@@ -53,6 +59,14 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.shadow-2 {
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.16), 0 3px 10px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
.shadow-3 {
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.19), 0 6px 10px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
@@ -77,7 +91,7 @@ body {
|
||||
background: linear-gradient(30deg, @background-color 0%, @background-secondary-color 100%);
|
||||
color: @text-color;
|
||||
line-height: 150%;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-family: @base-font;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -99,7 +113,13 @@ h2 {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#app {
|
||||
.app {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -222,7 +242,7 @@ h2 {
|
||||
}
|
||||
|
||||
text {
|
||||
font-family: Roboto;
|
||||
font-family: @base-font;
|
||||
fill: @text-secondary-color;
|
||||
|
||||
&.node-label {
|
||||
@@ -313,13 +333,13 @@ h2 {
|
||||
}
|
||||
|
||||
#details {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
z-index: 1024;
|
||||
display: block;
|
||||
right: 36px;
|
||||
right: @details-window-padding-left;
|
||||
top: 24px;
|
||||
bottom: 48px;
|
||||
width: 420px;
|
||||
width: @details-window-width;
|
||||
|
||||
.details-tools-wrapper {
|
||||
position: relative;
|
||||
@@ -327,8 +347,8 @@ h2 {
|
||||
|
||||
.details-tools {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 24px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
|
||||
span {
|
||||
.palable;
|
||||
@@ -347,6 +367,14 @@ h2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details-wrapper {
|
||||
height: 100%;
|
||||
padding-bottom: 8px;
|
||||
border-radius: 2px;
|
||||
background-color: #fff;
|
||||
.shadow-2;
|
||||
}
|
||||
}
|
||||
|
||||
.node-details {
|
||||
@@ -508,9 +536,112 @@ h2 {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal {
|
||||
|
||||
&-app {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
&-embedded {
|
||||
z-index: 512;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
top: 24px;
|
||||
bottom: 48px;
|
||||
left: 36px;
|
||||
right: (@details-window-width + @details-window-padding-left + 4px);
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1;
|
||||
border: 0px solid #000000;
|
||||
border-radius: 4px;
|
||||
color: #f0f0f0;
|
||||
font-family: @mono-font;
|
||||
.shadow-2;
|
||||
}
|
||||
|
||||
&-header {
|
||||
.truncate;
|
||||
text-align: center;
|
||||
color: @white;
|
||||
padding: 8px 24px;
|
||||
background-color: @text-color;
|
||||
position: relative;
|
||||
|
||||
&-title {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&-tools {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
|
||||
&-icon {
|
||||
.palable;
|
||||
padding: 4px;
|
||||
color: @white;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
border: 1px solid rgba(255, 255, 255, 0);
|
||||
border-radius: 10%;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-inner {
|
||||
flex: 1;
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
padding: 8px;
|
||||
|
||||
.terminal {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-status-bar {
|
||||
font-family: @base-font;
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
width: 50%;
|
||||
height: 130px;
|
||||
padding: 16px;
|
||||
opacity: 0.8;
|
||||
|
||||
h1 {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: @white;
|
||||
}
|
||||
}
|
||||
|
||||
&-cursor {
|
||||
color: #000;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-inactive .terminal-cursor {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
||||
19
client/build/terminal.html
Normal file
19
client/build/terminal.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Weave Scope Terminal</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 10]>
|
||||
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
<![endif]-->
|
||||
<div class="wrap">
|
||||
<div id="app"></div>
|
||||
</div>
|
||||
|
||||
<script src="terminal-app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,6 +6,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.1",
|
||||
"d3": "~3.5.5",
|
||||
"dagre": "0.7.4",
|
||||
"debug": "~2.2.0",
|
||||
|
||||
@@ -10,6 +10,7 @@ var app = express();
|
||||
*
|
||||
* Express routes for:
|
||||
* - app.js
|
||||
* - app-terminal.js
|
||||
* - index.html
|
||||
*
|
||||
* Proxy requests to:
|
||||
@@ -18,11 +19,12 @@ var app = express();
|
||||
************************************************************/
|
||||
|
||||
// Serve application file depending on environment
|
||||
app.get('/app.js', function(req, res) {
|
||||
app.get(/(app|terminal-app).js/, function(req, res) {
|
||||
var filename = req.originalUrl;
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
res.sendFile(__dirname + '/build/app.js');
|
||||
res.sendFile(__dirname + '/build' + filename);
|
||||
} else {
|
||||
res.redirect('//localhost:4041/build/app.js');
|
||||
res.redirect('//localhost:4041/build' + filename);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -25,17 +25,24 @@ module.exports = {
|
||||
devtool: 'cheap-module-source-map',
|
||||
|
||||
// Set entry point include necessary files for hot load
|
||||
entry: [
|
||||
'webpack-dev-server/client?http://localhost:4041',
|
||||
'webpack/hot/only-dev-server',
|
||||
'./app/scripts/main'
|
||||
],
|
||||
entry: {
|
||||
'app': [
|
||||
'./app/scripts/main',
|
||||
'webpack-dev-server/client?http://localhost:4041',
|
||||
'webpack/hot/only-dev-server'
|
||||
],
|
||||
'terminal-app': [
|
||||
'./app/scripts/terminal-main',
|
||||
'webpack-dev-server/client?http://localhost:4041',
|
||||
'webpack/hot/only-dev-server'
|
||||
]
|
||||
},
|
||||
|
||||
// This will not actually create a app.js file in ./build. It is used
|
||||
// by the dev server for dynamic hot loading.
|
||||
output: {
|
||||
path: path.join(__dirname, 'build/'),
|
||||
filename: 'app.js',
|
||||
filename: '[name].js',
|
||||
publicPath: 'http://localhost:4041/build/'
|
||||
},
|
||||
|
||||
@@ -51,7 +58,7 @@ module.exports = {
|
||||
preLoaders: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
exclude: /node_modules|vendor/,
|
||||
loader: 'eslint-loader'
|
||||
}
|
||||
],
|
||||
@@ -74,7 +81,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
exclude: /node_modules|vendor/,
|
||||
loaders: ['react-hot', 'babel']
|
||||
}
|
||||
]
|
||||
|
||||
@@ -15,18 +15,21 @@ module.exports = {
|
||||
// fail on first error when building release
|
||||
bail: true,
|
||||
|
||||
entry: './app/scripts/main',
|
||||
entry: {
|
||||
app: './app/scripts/main',
|
||||
'terminal-app': './app/scripts/terminal-main'
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, 'build/'),
|
||||
filename: 'app.js'
|
||||
filename: '[name].js'
|
||||
},
|
||||
|
||||
module: {
|
||||
preLoaders: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
exclude: /node_modules|vendor/,
|
||||
loader: 'eslint-loader'
|
||||
}
|
||||
],
|
||||
@@ -43,7 +46,7 @@ module.exports = {
|
||||
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
loader: 'file-loader'
|
||||
},
|
||||
{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel' }
|
||||
{ test: /\.jsx?$/, exclude: /node_modules|vendor/, loader: 'babel' }
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user