Merge pull request #1966 from weaveworks/746-resize-ttys

Resize TTYs
This commit is contained in:
Simon
2016-11-03 11:06:16 +01:00
committed by GitHub
59 changed files with 4393 additions and 116 deletions

View File

@@ -580,16 +580,17 @@ export function receiveControlNodeRemoved(nodeId) {
};
}
export function receiveControlPipeFromParams(pipeId, rawTty) {
export function receiveControlPipeFromParams(pipeId, rawTty, resizeTtyControl) {
// TODO add nodeId
return {
type: ActionTypes.RECEIVE_CONTROL_PIPE,
pipeId,
rawTty
rawTty,
resizeTtyControl
};
}
export function receiveControlPipe(pipeId, nodeId, rawTty) {
export function receiveControlPipe(pipeId, nodeId, rawTty, resizeTtyControl) {
return (dispatch, getState) => {
const state = getState();
if (state.get('nodeDetails').last()
@@ -608,7 +609,8 @@ export function receiveControlPipe(pipeId, nodeId, rawTty) {
type: ActionTypes.RECEIVE_CONTROL_PIPE,
nodeId,
pipeId,
rawTty
rawTty,
resizeTtyControl
});
updateRoute(getState);

View File

@@ -204,46 +204,47 @@ export default class NodeDetailsTable extends React.Component {
}
renderHeaders(sortBy, sortedDesc) {
if (this.props.nodes && this.props.nodes.length > 0) {
const headers = this.getColumnHeaders();
const colStyles = getColumnsStyles(headers);
return (
<tr>
{headers.map((header, i) => {
const headerClasses = ['node-details-table-header', 'truncate'];
const onHeaderClick = ev => {
this.handleHeaderClick(ev, header.id, sortBy, sortedDesc);
};
// sort by first metric by default
const isSorted = header.id === sortBy;
const isSortedDesc = isSorted && sortedDesc;
const isSortedAsc = isSorted && !isSortedDesc;
if (isSorted) {
headerClasses.push('node-details-table-header-sorted');
}
const style = colStyles[i];
const label = (style.width === CW.XS && XS_LABEL[header.id]) ?
XS_LABEL[header.id] :
header.label;
return (
<td className={headerClasses.join(' ')} style={style} onClick={onHeaderClick}
title={header.label} key={header.id}>
{isSortedAsc
&& <span className="node-details-table-header-sorter fa fa-caret-up" />}
{isSortedDesc
&& <span className="node-details-table-header-sorter fa fa-caret-down" />}
{label}
</td>
);
})}
</tr>
);
if (!this.props.nodes || this.props.nodes.length === 0) {
return null;
}
return '';
const headers = this.getColumnHeaders();
const colStyles = getColumnsStyles(headers);
return (
<tr>
{headers.map((header, i) => {
const headerClasses = ['node-details-table-header', 'truncate'];
const onHeaderClick = ev => {
this.handleHeaderClick(ev, header.id, sortBy, sortedDesc);
};
// sort by first metric by default
const isSorted = header.id === sortBy;
const isSortedDesc = isSorted && sortedDesc;
const isSortedAsc = isSorted && !isSortedDesc;
if (isSorted) {
headerClasses.push('node-details-table-header-sorted');
}
const style = colStyles[i];
const label = (style.width === CW.XS && XS_LABEL[header.id]) ?
XS_LABEL[header.id] :
header.label;
return (
<td className={headerClasses.join(' ')} style={style} onClick={onHeaderClick}
title={header.label} key={header.id}>
{isSortedAsc
&& <span className="node-details-table-header-sorter fa fa-caret-up" />}
{isSortedDesc
&& <span className="node-details-table-header-sorter fa fa-caret-down" />}
{label}
</td>
);
})}
</tr>
);
}
render() {

View File

@@ -13,7 +13,8 @@ class TerminalApp extends React.Component {
const paramString = window.location.hash.split('/').pop();
const params = JSON.parse(decodeURIComponent(paramString));
this.props.receiveControlPipeFromParams(params.pipe.id, null, params.pipe.raw, false);
this.props.receiveControlPipeFromParams(params.pipe.id, params.pipe.raw,
params.pipe.resizeTtyControl);
this.state = {
title: params.title,

View File

@@ -8,7 +8,7 @@ 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 { getPipeStatus, basePath, doResizeTty } from '../utils/web-api-utils';
import Term from 'xterm';
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
@@ -32,27 +32,29 @@ function ab2str(buf) {
return decodedString;
}
function terminalCellSize(wrapperNode, rows, cols) {
const height = wrapperNode.clientHeight;
function terminalCellSize(wrapperNode) {
// Badly guess the width/height of the row.
let characterWidth = 20;
let characterHeight = 20;
// Guess the width of the row.
let width = wrapperNode.clientWidth;
// Now try and measure the first row we find.
const firstRow = wrapperNode.querySelector('.terminal div');
if (!firstRow) {
const subjectRow = wrapperNode.querySelector('.terminal .xterm-rows div');
if (!subjectRow) {
log("ERROR: Couldn't find first row, resizing might not work very well.");
} else {
const rowDisplay = firstRow.style.display;
firstRow.style.display = 'inline';
width = firstRow.offsetWidth;
firstRow.style.display = rowDisplay;
const rowDisplay = subjectRow.style.display;
const contentBuffer = subjectRow.innerHTML;
subjectRow.innerHTML = 'W';
subjectRow.style.display = 'inline';
characterWidth = subjectRow.getBoundingClientRect().width;
subjectRow.style.display = rowDisplay;
characterHeight = parseInt(subjectRow.offsetHeight, 10);
subjectRow.innerHTML = contentBuffer;
}
const pixelPerCol = width / cols;
const pixelPerRow = height / rows;
log('Caculated (col, row) sizes in px: ', pixelPerCol, pixelPerRow);
return {pixelPerCol, pixelPerRow};
log('Caculated (charWidth, charHeight) sizes in px: ', characterWidth, characterHeight);
return {characterWidth, characterHeight};
}
@@ -88,14 +90,13 @@ class Terminal extends React.Component {
connected: false,
rows: DEFAULT_ROWS,
cols: DEFAULT_COLS,
pixelPerCol: 0,
pixelPerRow: 0
characterWidth: 0,
characterHeight: 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) {
@@ -200,15 +201,14 @@ class Terminal extends React.Component {
this.createWebsocket(this.term);
const {pixelPerCol, pixelPerRow} = terminalCellSize(
this.term.element, this.state.rows, this.state.cols);
const {characterWidth, characterHeight} = terminalCellSize(this.term.element);
window.addEventListener('resize', this.handleResize);
this.resizeTimeout = setTimeout(() => {
this.setState({
pixelPerCol,
pixelPerRow
characterWidth,
characterHeight
});
this.handleResize();
}, 10);
@@ -256,29 +256,29 @@ 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);
this.props.dispatch(clickCloseTerminal(this.getPipeId()));
const bcr = ReactDOM.findDOMNode(this).getBoundingClientRect();
const minWidth = this.state.pixelPerCol * 80 + (8 * 2);
const minWidth = this.state.characterWidth * 80 + (8 * 2);
openNewWindow(`terminal.html#!/state/${paramString}`, bcr, minWidth);
}
handleResize() {
const innerNode = ReactDOM.findDOMNode(this.innerFlex);
// scrollbar === 16px
const width = innerNode.clientWidth - (2 * 8) - 16;
const height = innerNode.clientHeight - (2 * 8);
const cols = DEFAULT_COLS;
const rows = Math.floor(height / this.state.pixelPerRow);
const cols = Math.floor(width / this.state.characterWidth);
const rows = Math.floor(height / this.state.characterHeight);
this.setState({cols, rows});
const resizeTtyControl = this.props.pipe.get('resizeTtyControl');
if (resizeTtyControl) {
doResizeTty(this.getPipeId(), resizeTtyControl, cols, rows)
.then(() => this.setState({cols, rows}));
}
}
isEmbedded() {
@@ -359,7 +359,7 @@ class Terminal extends React.Component {
overflow: 'hidden',
};
const innerStyle = {
width: (this.state.cols + 2) * this.state.pixelPerCol
width: (this.state.cols + 2) * this.state.characterWidth
};
const innerClassName = classNames('terminal-inner hideable', {
'terminal-inactive': !this.state.connected
@@ -369,7 +369,6 @@ class Terminal extends React.Component {
<div className="terminal-wrapper">
{this.isEmbedded() && this.getTerminalHeader()}
<div
onClick={this.focusTerminal}
ref={(ref) => this.innerFlex = ref}
className={innerClassName}
style={innerFlexStyle} >

View File

@@ -466,7 +466,8 @@ export function rootReducer(state = initialState, action) {
return state.setIn(['controlPipes', action.pipeId], makeOrderedMap({
id: action.pipeId,
nodeId: action.nodeId,
raw: action.rawTty
raw: action.rawTty,
resizeTtyControl: action.resizeTtyControl,
}));
}

View File

@@ -232,7 +232,11 @@ export function doControlRequest(nodeId, control, dispatch) {
if (res) {
if (res.pipe) {
dispatch(blurSearch());
dispatch(receiveControlPipe(res.pipe, nodeId, res.raw_tty, true));
dispatch(receiveControlPipe(
res.pipe,
nodeId,
res.raw_tty,
{id: res.resize_tty_control, probeId: control.probeId, nodeId: control.nodeId}));
}
if (res.removedNode) {
dispatch(receiveControlNodeRemoved(nodeId));
@@ -248,6 +252,22 @@ export function doControlRequest(nodeId, control, dispatch) {
});
}
export function doResizeTty(pipeId, control, cols, rows) {
const url = `api/control/${encodeURIComponent(control.probeId)}/`
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
return reqwest({
method: 'POST',
url,
data: JSON.stringify({pipeID: pipeId, width: cols.toString(), height: rows.toString()}),
})
.fail((err) => {
log(`Error resizing pipe: ${err}`);
});
}
export function deletePipe(pipeId, dispatch) {
const url = `api/pipe/${encodeURIComponent(pipeId)}`;
reqwest({
@@ -263,6 +283,7 @@ export function deletePipe(pipeId, dispatch) {
});
}
export function getPipeStatus(pipeId, dispatch) {
const url = `api/pipe/${encodeURIComponent(pipeId)}/check`;
reqwest({