diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 3621f64e9..4220d859d 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -18,7 +18,6 @@ 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'; import { getRouter } from '../utils/router-utils'; import DebugToolbar, { showingDebugToolbar, toggleDebugToolbar } from './debug-toolbar.js'; @@ -103,7 +102,7 @@ class App extends React.Component { render() { const { gridMode, showingDetails, showingHelp, showingMetricsSelector, - showingNetworkSelector, showingTerminal } = this.props; + showingNetworkSelector } = this.props; const isIframe = window !== window.top; return ( @@ -114,8 +113,6 @@ class App extends React.Component { {showingDetails &&
} - {showingTerminal && } -
{!isIframe && diff --git a/client/app/scripts/components/details-card.js b/client/app/scripts/components/details-card.js index a8469625d..1179248c4 100644 --- a/client/app/scripts/components/details-card.js +++ b/client/app/scripts/components/details-card.js @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import NodeDetails from './node-details'; +import EmbeddedTerminal from './embedded-terminal'; import { DETAILS_PANEL_WIDTH as WIDTH, DETAILS_PANEL_OFFSET as OFFSET, DETAILS_PANEL_MARGINS as MARGINS } from '../constants/styles'; @@ -22,7 +23,7 @@ class DetailsCard extends React.Component { render() { let transform; - const origin = this.props.origin; + const { origin, showingTerminal } = this.props; const panelHeight = window.innerHeight - MARGINS.bottom - MARGINS.top; if (origin && !this.state.mounted) { // render small panel near origin, will transition into normal panel after being mounted @@ -45,12 +46,27 @@ class DetailsCard extends React.Component { transform = `translateX(${shiftX}px)`; } } + const style = { + transform, + left: showingTerminal ? MARGINS.right : null, + width: showingTerminal ? null : WIDTH + }; return ( -
+
+ {showingTerminal && }
); } } -export default connect()(DetailsCard); + +function mapStateToProps(state, props) { + const pipe = state.get('controlPipes').last(); + return { + showingTerminal: pipe && pipe.get('nodeId') === props.id, + }; +} + + +export default connect(mapStateToProps)(DetailsCard); diff --git a/client/app/scripts/components/embedded-terminal.js b/client/app/scripts/components/embedded-terminal.js index 2ad699449..149d5998e 100644 --- a/client/app/scripts/components/embedded-terminal.js +++ b/client/app/scripts/components/embedded-terminal.js @@ -1,33 +1,63 @@ import React from 'react'; import { connect } from 'react-redux'; -import { getNodeColor, getNodeColorDark } from '../utils/color-utils'; +import { brightenColor, getNodeColorDark } from '../utils/color-utils'; +import { DETAILS_PANEL_WIDTH, DETAILS_PANEL_MARGINS } from '../constants/styles'; import Terminal from './terminal'; -import { DETAILS_PANEL_WIDTH, DETAILS_PANEL_MARGINS, - DETAILS_PANEL_OFFSET } from '../constants/styles'; class EmeddedTerminal extends React.Component { + + constructor(props, context) { + super(props, context); + this.state = { + mounted: null, + animated: null, + }; + + this.handleTransitionEnd = this.handleTransitionEnd.bind(this); + } + + componentDidMount() { + setTimeout(() => { + this.setState({mounted: true}); + }); + } + + getTransform() { + const dx = this.state.mounted ? 0 : + window.innerWidth - DETAILS_PANEL_WIDTH - DETAILS_PANEL_MARGINS.right; + return `translateX(${dx}px)`; + } + + handleTransitionEnd() { + this.setState({ animated: true }); + } + render() { const { pipe, details } = this.props; const nodeId = pipe.get('nodeId'); const node = details.get(nodeId); const d = node && node.details; - const titleBarColor = d && getNodeColorDark(d.rank, d.label); - const statusBarColor = d && getNodeColor(d.rank, d.label); + const titleBarColor = d && getNodeColorDark(d.rank, d.label, d.pseudo); + const statusBarColor = d && brightenColor(titleBarColor); const title = d && d.label; - const style = { - right: DETAILS_PANEL_MARGINS.right + DETAILS_PANEL_WIDTH + 10 + - (details.size * DETAILS_PANEL_OFFSET) - }; - // 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 ( -
- +
+
+ +
); } diff --git a/client/app/scripts/components/terminal.js b/client/app/scripts/components/terminal.js index a514a7b3b..570cdd06c 100644 --- a/client/app/scripts/components/terminal.js +++ b/client/app/scripts/components/terminal.js @@ -88,9 +88,11 @@ class Terminal extends React.Component { pixelPerCol: 0, pixelPerRow: 0 }; + this.handleCloseClick = this.handleCloseClick.bind(this); this.handlePopoutTerminal = this.handlePopoutTerminal.bind(this); this.handleResize = this.handleResize.bind(this); + this.focusTerminal = this.focusTerminal.bind(this); } createWebsocket(term) { @@ -106,7 +108,13 @@ class Terminal extends React.Component { }; socket.onclose = () => { - log('socket closed'); + // + // componentWillUnmount has called close and tidied up! don't try and do it again here + // (setState etc), its too late. + // + if (!this.socket) { + return; + } this.socket = null; const wereConnected = this.state.connected; this.setState({connected: false}); @@ -133,16 +141,26 @@ class Terminal extends React.Component { this.socket = socket; } - componentDidMount() { - const component = this; + componentWillReceiveProps(nextProps) { + if (this.props.connect !== nextProps.connect && nextProps.connect) { + this.mountTerminal(); + } + } + componentDidMount() { + if (this.props.connect) { + this.mountTerminal(); + } + } + + mountTerminal() { this.term = new Term({ cols: this.state.cols, rows: this.state.rows, convertEol: !this.props.raw }); - const innerNode = ReactDOM.findDOMNode(component.inner); + const innerNode = ReactDOM.findDOMNode(this.inner); this.term.open(innerNode); this.term.on('data', (data) => { if (this.socket) { @@ -180,6 +198,7 @@ class Terminal extends React.Component { this.term.destroy(); this.term = null; } + if (this.socket) { log('close socket'); this.socket.close(); @@ -187,14 +206,6 @@ class Terminal extends React.Component { } } - componentWillReceiveProps(nextProps) { - const containerMarginChanged = nextProps.containerMargin !== this.props.containerMargin; - log(nextProps.containerMargin); - if (containerMarginChanged) { - this.handleResize(); - } - } - componentDidUpdate(prevProps, prevState) { const sizeChanged = ( prevState.cols !== this.state.cols || @@ -213,6 +224,12 @@ class Terminal extends React.Component { this.props.dispatch(clickCloseTerminal(this.getPipeId(), true)); } + focusTerminal() { + if (this.term) { + this.term.focus(); + } + } + handlePopoutTerminal(ev) { ev.preventDefault(); const paramString = JSON.stringify(this.props); @@ -247,8 +264,9 @@ class Terminal extends React.Component { } getTerminalHeader() { + const light = this.props.statusBarColor || getNeutralColor(); const style = { - backgroundColor: this.props.titleBarColor || getNeutralColor() + backgroundColor: light, }; return (
@@ -318,8 +336,11 @@ class Terminal extends React.Component { return (
{this.isEmbedded() && this.getTerminalHeader()} -
this.innerFlex = ref} - className={innerClassName} style={innerFlexStyle} > +
this.innerFlex = ref} + className={innerClassName} + style={innerFlexStyle} >
this.inner = ref} />
{this.getTerminalStatusBar()} @@ -328,4 +349,10 @@ class Terminal extends React.Component { } } + +Terminal.defaultProps = { + connect: true +}; + + export default connect()(Terminal); diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 975a04914..214cac3ba 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -41,7 +41,7 @@ @details-window-padding-left: 36px; @border-radius: 4px; -@terminal-header-height: 34px; +@terminal-header-height: 44px; @node-opacity-blurred: 0.25; @node-highlight-fill-opacity: 0.1; @@ -578,17 +578,18 @@ h2 { .details { &-wrapper { position: fixed; + display: flex; z-index: 1024; right: @details-window-padding-left; top: 24px; bottom: 48px; - width: @details-window-width; transition: transform 0.33333s cubic-bezier(0,0,0.21,1); } } .node-details { height: 100%; + width: @details-window-width; background-color: rgba(255, 255, 255, 0.86); display: flex; flex-flow: column; @@ -597,6 +598,8 @@ h2 { border-radius: 2px; background-color: #fff; .shadow-2; + // keep node-details above the terminal. + z-index: 2; &:last-child { margin-bottom: 0; @@ -979,35 +982,38 @@ h2 { } &-embedded { - z-index: 512; - position: fixed; - top: 24px; - bottom: 48px; - left: 36px; - right: (@details-window-width + @details-window-padding-left + 10px); + position: relative; + // shadow of animation-wrapper is 10px, let it fit in here without being + // overflow hiddened. + padding: 10px 0 10px 10px; + flex: 1; + overflow-x: hidden; + } + + &-animation-wrapper { + width: 100%; + height: 100%; + transition: transform 0.5s cubic-bezier(0.230, 1.000, 0.320, 1.000); + .shadow-2; } &-wrapper { - position: absolute; - bottom: 0; - top: 0; - left: 0; - right: 0; + width: 100%; + height: 100%; border: 0px solid #000000; - border-radius: 4px; color: #f0f0f0; - font-family: @mono-font; - .shadow-2; } &-header { .truncate; - text-align: center; color: @white; height: @terminal-header-height; padding: 8px 24px; background-color: @text-color; position: relative; + font-size: 14px; + line-height: 28px; + border-radius: 4px 0 0 0; &-title { cursor: default; @@ -1030,7 +1036,6 @@ h2 { font-size: 0.8em; font-weight: bold; text-transform: uppercase; - word-spacing: -4px; &:hover { opacity: 1; @@ -1047,13 +1052,15 @@ h2 { &-embedded &-inner { top: @terminal-header-height; } &-app &-inner { top: 0; } &-inner { + cursor: text; + font-family: @mono-font; position: absolute; bottom: 0; left: 0; right: 0; background-color: rgba(0, 0, 0, 0.93); padding: 8px; - border-radius: 0 0 2px 2px; + border-radius: 0 0 0 4px; .terminal { background-color: transparent !important;