mirror of https://github.com/portainer/portainer
EE-1976 fix(docker): support copy and paste in container console
parent
93866644c6
commit
ac99b9561e
|
@ -1,6 +1,6 @@
|
||||||
import 'bootstrap/dist/css/bootstrap.css';
|
import 'bootstrap/dist/css/bootstrap.css';
|
||||||
import 'toastr/build/toastr.css';
|
import 'toastr/build/toastr.css';
|
||||||
import 'xterm/dist/xterm.css';
|
import 'xterm/css/xterm.css';
|
||||||
import 'angularjs-slider/dist/rzslider.css';
|
import 'angularjs-slider/dist/rzslider.css';
|
||||||
import 'angular-json-tree/dist/angular-json-tree.css';
|
import 'angular-json-tree/dist/angular-json-tree.css';
|
||||||
import 'angular-loading-bar/build/loading-bar.css';
|
import 'angular-loading-bar/build/loading-bar.css';
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { Terminal } from 'xterm';
|
|
||||||
import * as fit from 'xterm/lib/addons/fit/fit';
|
|
||||||
import { agentInterceptor } from './portainer/services/axios';
|
import { agentInterceptor } from './portainer/services/axios';
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -27,8 +25,6 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService
|
||||||
request: agentInterceptor,
|
request: agentInterceptor,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Terminal.applyAddon(fit);
|
|
||||||
|
|
||||||
$uibTooltipProvider.setTriggers({
|
$uibTooltipProvider.setTriggers({
|
||||||
mouseenter: 'mouseleave',
|
mouseenter: 'mouseleave',
|
||||||
click: 'click',
|
click: 'click',
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||||
|
|
||||||
angular.module('portainer.docker').controller('ContainerConsoleController', [
|
angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||||
|
@ -28,7 +29,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||||
CONSOLE_COMMANDS_LABEL_PREFIX,
|
CONSOLE_COMMANDS_LABEL_PREFIX,
|
||||||
SidebarService
|
SidebarService
|
||||||
) {
|
) {
|
||||||
var socket, term;
|
var socket, term, fitAddon;
|
||||||
|
|
||||||
let states = Object.freeze({
|
let states = Object.freeze({
|
||||||
disconnected: 0,
|
disconnected: 0,
|
||||||
|
@ -138,6 +139,9 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||||
term.write('\n\r(connection closed)');
|
term.write('\n\r(connection closed)');
|
||||||
term.dispose();
|
term.dispose();
|
||||||
}
|
}
|
||||||
|
if (fitAddon) {
|
||||||
|
fitAddon.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -152,7 +156,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||||
function resize(restcall, add) {
|
function resize(restcall, add) {
|
||||||
add = add || 0;
|
add = add || 0;
|
||||||
|
|
||||||
term.fit();
|
fitAddon.fit();
|
||||||
var termWidth = term.cols;
|
var termWidth = term.cols;
|
||||||
var termHeight = 30;
|
var termHeight = 30;
|
||||||
term.resize(termWidth, termHeight);
|
term.resize(termWidth, termHeight);
|
||||||
|
@ -161,7 +165,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTerm(url, resizeRestCall) {
|
function initTerm(url, resizeRestCall) {
|
||||||
let resizefun = resize.bind(this, resizeRestCall);
|
let resizeFunc = resize.bind(this, resizeRestCall);
|
||||||
|
|
||||||
if ($transition$.params().nodeName) {
|
if ($transition$.params().nodeName) {
|
||||||
url += '&nodeName=' + $transition$.params().nodeName;
|
url += '&nodeName=' + $transition$.params().nodeName;
|
||||||
|
@ -176,23 +180,24 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||||
|
|
||||||
socket.onopen = function () {
|
socket.onopen = function () {
|
||||||
$scope.state = states.connected;
|
$scope.state = states.connected;
|
||||||
term = new Terminal();
|
term = new Terminal({ cursorBlink: true });
|
||||||
|
fitAddon = new FitAddon();
|
||||||
|
term.loadAddon(fitAddon);
|
||||||
|
|
||||||
term.on('data', function (data) {
|
term.onData(function (data) {
|
||||||
socket.send(data);
|
socket.send(data);
|
||||||
});
|
});
|
||||||
var terminal_container = document.getElementById('terminal-container');
|
var terminal_container = document.getElementById('terminal-container');
|
||||||
term.open(terminal_container);
|
term.open(terminal_container);
|
||||||
term.focus();
|
term.focus();
|
||||||
term.setOption('cursorBlink', true);
|
|
||||||
|
|
||||||
window.onresize = function () {
|
window.onresize = function () {
|
||||||
resizefun();
|
resizeFunc();
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.$watch(SidebarService.isSidebarOpen, function () {
|
$scope.$watch(SidebarService.isSidebarOpen, function () {
|
||||||
setTimeout(resizefun, 400);
|
setTimeout(resizeFunc, 400);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.onmessage = function (e) {
|
socket.onmessage = function (e) {
|
||||||
|
@ -208,7 +213,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
};
|
};
|
||||||
|
|
||||||
resizefun(1);
|
resizeFunc(1);
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { fit } from 'xterm/lib/addons/fit/fit';
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { RotateCw, X, Terminal as TerminalIcon } from 'lucide-react';
|
import { RotateCw, X, Terminal as TerminalIcon } from 'lucide-react';
|
||||||
|
@ -29,7 +29,8 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KubeCtlShell({ environmentId, onClose }: Props) {
|
export function KubeCtlShell({ environmentId, onClose }: Props) {
|
||||||
const [terminal] = useState(new Terminal());
|
const [terminal] = useState(new Terminal({ cursorBlink: true }));
|
||||||
|
const [fitAddon] = useState(new FitAddon());
|
||||||
|
|
||||||
const [shell, setShell] = useState<ShellState>({
|
const [shell, setShell] = useState<ShellState>({
|
||||||
socket: null,
|
socket: null,
|
||||||
|
@ -46,22 +47,23 @@ export function KubeCtlShell({ environmentId, onClose }: Props) {
|
||||||
terminalClose(); // only css trick
|
terminalClose(); // only css trick
|
||||||
socket?.close();
|
socket?.close();
|
||||||
terminal.dispose();
|
terminal.dispose();
|
||||||
|
fitAddon.dispose();
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose, terminal, socket]);
|
}, [onClose, socket, terminal, fitAddon]);
|
||||||
|
|
||||||
const openTerminal = useCallback(() => {
|
const openTerminal = useCallback(() => {
|
||||||
if (!terminalElem.current) {
|
if (!terminalElem.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
terminal.open(terminalElem.current);
|
terminal.open(terminalElem.current);
|
||||||
terminal.setOption('cursorBlink', true);
|
|
||||||
terminal.focus();
|
terminal.focus();
|
||||||
fit(terminal);
|
fitAddon.fit();
|
||||||
terminal.writeln('#Run kubectl commands inside here');
|
terminal.writeln('#Run kubectl commands inside here');
|
||||||
terminal.writeln('#e.g. kubectl get all');
|
terminal.writeln('#e.g. kubectl get all');
|
||||||
terminal.writeln('');
|
terminal.writeln('');
|
||||||
}, [terminal]);
|
}, [terminal, fitAddon]);
|
||||||
|
|
||||||
// refresh socket listeners on socket updates
|
// refresh socket listeners on socket updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -106,7 +108,17 @@ export function KubeCtlShell({ environmentId, onClose }: Props) {
|
||||||
const socket = new WebSocket(buildUrl(jwt, environmentId));
|
const socket = new WebSocket(buildUrl(jwt, environmentId));
|
||||||
setShell((shell) => ({ ...shell, socket }));
|
setShell((shell) => ({ ...shell, socket }));
|
||||||
|
|
||||||
terminal.onData((data) => socket.send(data));
|
terminal.onData((data) => {
|
||||||
|
if (
|
||||||
|
terminal.modes.bracketedPasteMode &&
|
||||||
|
data.slice(0, 6) === '\x1b[200~' &&
|
||||||
|
data.slice(-6) === '\x1b[201~'
|
||||||
|
) {
|
||||||
|
socket.send(data.slice(6, -6));
|
||||||
|
} else {
|
||||||
|
socket.send(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
terminal.onKey(({ domEvent }) => {
|
terminal.onKey(({ domEvent }) => {
|
||||||
if (domEvent.ctrlKey && domEvent.code === 'KeyD') {
|
if (domEvent.ctrlKey && domEvent.code === 'KeyD') {
|
||||||
close();
|
close();
|
||||||
|
@ -118,11 +130,12 @@ export function KubeCtlShell({ environmentId, onClose }: Props) {
|
||||||
function close() {
|
function close() {
|
||||||
socket.close();
|
socket.close();
|
||||||
terminal.dispose();
|
terminal.dispose();
|
||||||
|
fitAddon.dispose();
|
||||||
window.removeEventListener('resize', terminalResize);
|
window.removeEventListener('resize', terminalResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
return close;
|
return close;
|
||||||
}, [environmentId, jwt, terminal]);
|
}, [environmentId, jwt, terminal, fitAddon]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.root, { [styles.minimized]: shell.minimized })}>
|
<div className={clsx(styles.root, { [styles.minimized]: shell.minimized })}>
|
||||||
|
|
|
@ -118,7 +118,9 @@
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"toastr": "^2.1.4",
|
"toastr": "^2.1.4",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
"xterm": "^3.8.0",
|
"x256": "^0.0.2",
|
||||||
|
"xterm": "^4.1.3",
|
||||||
|
"xterm-addon-fit": "^0.5.0",
|
||||||
"yaml": "^1.10.2",
|
"yaml": "^1.10.2",
|
||||||
"yup": "^0.32.11",
|
"yup": "^0.32.11",
|
||||||
"zustand": "^4.1.1"
|
"zustand": "^4.1.1"
|
||||||
|
@ -234,4 +236,4 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": "last 2 versions"
|
"browserslist": "last 2 versions"
|
||||||
}
|
}
|
13
yarn.lock
13
yarn.lock
|
@ -18932,10 +18932,15 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||||
|
|
||||||
xterm@^3.8.0:
|
xterm-addon-fit@^0.5.0:
|
||||||
version "3.14.5"
|
version "0.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.14.5.tgz#c9d14e48be6873aa46fb429f22f2165557fd2dea"
|
resolved "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
|
||||||
integrity sha512-DVmQ8jlEtL+WbBKUZuMxHMBgK/yeIZwkXB81bH+MGaKKnJGYwA+770hzhXPfwEIokK9On9YIFPRleVp/5G7z9g==
|
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
|
||||||
|
|
||||||
|
xterm@^4.1.3:
|
||||||
|
version "4.19.0"
|
||||||
|
resolved "https://registry.npmmirror.com/xterm/-/xterm-4.19.0.tgz#c0f9d09cd61de1d658f43ca75f992197add9ef6d"
|
||||||
|
integrity sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==
|
||||||
|
|
||||||
xterm@^4.13.0:
|
xterm@^4.13.0:
|
||||||
version "4.17.0"
|
version "4.17.0"
|
||||||
|
|
Loading…
Reference in New Issue