mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
feat(UI): migrate console view to react EE-2276 (#8767)
This commit is contained in:
parent
c03b2ebbc1
commit
926ca19a1b
9 changed files with 206 additions and 212 deletions
|
@ -94,7 +94,8 @@ body,
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-horizontal .control-label.text-left {
|
.form-horizontal .control-label.text-left,
|
||||||
|
.form-row .control-label.text-left {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,7 +174,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
||||||
url: '/:pod/:container/console',
|
url: '/:pod/:container/console',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'kubernetesApplicationConsoleView',
|
component: 'kubernetesConsoleView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { IngressesDatatableView } from '@/react/kubernetes/ingresses/IngressData
|
||||||
import { CreateIngressView } from '@/react/kubernetes/ingresses/CreateIngressView';
|
import { CreateIngressView } from '@/react/kubernetes/ingresses/CreateIngressView';
|
||||||
import { DashboardView } from '@/react/kubernetes/DashboardView';
|
import { DashboardView } from '@/react/kubernetes/DashboardView';
|
||||||
import { ServicesView } from '@/react/kubernetes/ServicesView';
|
import { ServicesView } from '@/react/kubernetes/ServicesView';
|
||||||
|
import { ConsoleView } from '@/react/kubernetes/applications/ConsoleView';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
.module('portainer.kubernetes.react.views', [])
|
.module('portainer.kubernetes.react.views', [])
|
||||||
|
@ -29,4 +30,8 @@ export const viewsModule = angular
|
||||||
.component(
|
.component(
|
||||||
'kubernetesDashboardView',
|
'kubernetesDashboardView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(DashboardView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(DashboardView))), [])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'kubernetesConsoleView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(ConsoleView))), [])
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
<page-header
|
|
||||||
ng-if="ctrl.state.viewReady"
|
|
||||||
title="'Application console'"
|
|
||||||
breadcrumbs="[
|
|
||||||
{ label:'Namespaces', link:'kubernetes.resourcePools' },
|
|
||||||
{
|
|
||||||
label:ctrl.application.ResourcePool,
|
|
||||||
link: 'kubernetes.resourcePools.resourcePool',
|
|
||||||
linkParams:{ id: ctrl.application.ResourcePool }
|
|
||||||
},
|
|
||||||
{ label:'Applications', link:'kubernetes.applications' },
|
|
||||||
{
|
|
||||||
label:ctrl.application.Name,
|
|
||||||
link: 'kubernetes.applications.application',
|
|
||||||
linkParams:{ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool }
|
|
||||||
},
|
|
||||||
'Pods',
|
|
||||||
ctrl.podName,
|
|
||||||
'Containers',
|
|
||||||
ctrl.containerName,
|
|
||||||
'Console'
|
|
||||||
]"
|
|
||||||
reload="true"
|
|
||||||
>
|
|
||||||
</page-header>
|
|
||||||
|
|
||||||
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
|
|
||||||
|
|
||||||
<div ng-if="ctrl.state.viewReady">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body>
|
|
||||||
<form class="form-horizontal" autocomplete="off">
|
|
||||||
<div class="col-sm-12 form-section-title"> Console </div>
|
|
||||||
<!-- Command -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="console_command" class="col-sm-3 col-lg-2 control-label text-left">Command</label>
|
|
||||||
<div class="col-sm-8 input-group">
|
|
||||||
<span class="input-group-addon">
|
|
||||||
<pr-icon icon="'terminal'" class="mr-1"></pr-icon>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="/bin/bash"
|
|
||||||
ng-model="ctrl.state.command"
|
|
||||||
name="console_command"
|
|
||||||
uib-typeahead="command for command in ctrl.state.availableCommands | filter:$viewValue | limitTo:5"
|
|
||||||
typeahead-min-length="0"
|
|
||||||
auto-focus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !command -->
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
style="margin: 0"
|
|
||||||
ng-if="!ctrl.state.connected"
|
|
||||||
ng-disabled="!ctrl.state.command || ctrl.state.connected"
|
|
||||||
ng-click="ctrl.connectConsole()"
|
|
||||||
button-spinner="ctrl.state.actionInProgress"
|
|
||||||
>
|
|
||||||
<span ng-hide="ctrl.state.actionInProgress">Connect</span>
|
|
||||||
<span ng-show="ctrl.state.actionInProgress">Connection in progress...</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-primary btn-sm" style="margin: 0" ng-if="ctrl.state.connected" ng-click="ctrl.disconnect()"> Disconnect </button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<div id="terminal-container" class="terminal-container"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,9 +0,0 @@
|
||||||
angular.module('portainer.kubernetes').component('kubernetesApplicationConsoleView', {
|
|
||||||
templateUrl: './console.html',
|
|
||||||
controller: 'KubernetesApplicationConsoleController',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
bindings: {
|
|
||||||
$transition$: '<',
|
|
||||||
endpoint: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,117 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
import { Terminal } from 'xterm';
|
|
||||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
|
||||||
|
|
||||||
class KubernetesApplicationConsoleController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor($async, $state, Notifications, KubernetesApplicationService, LocalStorage) {
|
|
||||||
this.$async = $async;
|
|
||||||
this.$state = $state;
|
|
||||||
this.Notifications = Notifications;
|
|
||||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
|
||||||
this.LocalStorage = LocalStorage;
|
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
this.state.socket.close();
|
|
||||||
this.state.term.dispose();
|
|
||||||
this.state.connected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
configureSocketAndTerminal(socket, term) {
|
|
||||||
socket.onopen = function () {
|
|
||||||
const terminal_container = document.getElementById('terminal-container');
|
|
||||||
term.open(terminal_container);
|
|
||||||
term.setOption('cursorBlink', true);
|
|
||||||
term.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
term.on('data', function (data) {
|
|
||||||
socket.send(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.onmessage = function (msg) {
|
|
||||||
term.write(msg.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onerror = function (err) {
|
|
||||||
this.disconnect();
|
|
||||||
this.Notifications.error('Failure', err, 'Websocket connection error');
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
this.state.socket.onclose = function () {
|
|
||||||
this.disconnect();
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
this.state.connected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectConsole() {
|
|
||||||
const params = {
|
|
||||||
token: this.LocalStorage.getJWT(),
|
|
||||||
endpointId: this.endpoint.Id,
|
|
||||||
namespace: this.application.ResourcePool,
|
|
||||||
podName: this.podName,
|
|
||||||
containerName: this.containerName,
|
|
||||||
command: this.state.command,
|
|
||||||
};
|
|
||||||
|
|
||||||
const base = window.location.origin.startsWith('http') ? `${window.location.origin}${baseHref()}` : baseHref();
|
|
||||||
|
|
||||||
let url =
|
|
||||||
base +
|
|
||||||
'api/websocket/pod?' +
|
|
||||||
Object.keys(params)
|
|
||||||
.map((k) => k + '=' + params[k])
|
|
||||||
.join('&');
|
|
||||||
if (url.indexOf('https') > -1) {
|
|
||||||
url = url.replace('https://', 'wss://');
|
|
||||||
} else {
|
|
||||||
url = url.replace('http://', 'ws://');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.socket = new WebSocket(url);
|
|
||||||
this.state.term = new Terminal();
|
|
||||||
|
|
||||||
this.configureSocketAndTerminal(this.state.socket, this.state.term);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onInit() {
|
|
||||||
const availableCommands = ['/bin/bash', '/bin/sh'];
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
actionInProgress: false,
|
|
||||||
availableCommands: availableCommands,
|
|
||||||
command: availableCommands[1],
|
|
||||||
connected: false,
|
|
||||||
socket: null,
|
|
||||||
term: null,
|
|
||||||
viewReady: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const podName = this.$transition$.params().pod;
|
|
||||||
const applicationName = this.$transition$.params().name;
|
|
||||||
const namespace = this.$transition$.params().namespace;
|
|
||||||
const containerName = this.$transition$.params().container;
|
|
||||||
|
|
||||||
this.podName = podName;
|
|
||||||
this.containerName = containerName;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.application = await this.KubernetesApplicationService.get(namespace, applicationName);
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve application logs');
|
|
||||||
} finally {
|
|
||||||
this.state.viewReady = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
return this.$async(this.onInit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KubernetesApplicationConsoleController;
|
|
||||||
angular.module('portainer.kubernetes').controller('KubernetesApplicationConsoleController', KubernetesApplicationConsoleController);
|
|
197
app/react/kubernetes/applications/ConsoleView/ConsoleView.tsx
Normal file
197
app/react/kubernetes/applications/ConsoleView/ConsoleView.tsx
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
import { Terminal as TerminalIcon } from 'lucide-react';
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
|
||||||
|
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
|
||||||
|
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||||
|
import { notifyError } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
import { Widget, WidgetBody } from '@@/Widget';
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
|
||||||
|
interface StringDictionary {
|
||||||
|
[index: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConsoleView() {
|
||||||
|
const {
|
||||||
|
params: {
|
||||||
|
endpointId: environmentId,
|
||||||
|
container,
|
||||||
|
name: appName,
|
||||||
|
namespace,
|
||||||
|
pod: podID,
|
||||||
|
},
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
|
||||||
|
const [jwtToken] = useLocalStorage('JWT', '');
|
||||||
|
const [command, setCommand] = useState('/bin/sh');
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState('closed');
|
||||||
|
const [terminal, setTerminal] = useState(null as Terminal | null);
|
||||||
|
const [socket, setSocket] = useState(null as WebSocket | null);
|
||||||
|
|
||||||
|
const breadcrumbs = [
|
||||||
|
{
|
||||||
|
label: 'Namespaces',
|
||||||
|
link: 'kubernetes.resourcePools',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: namespace,
|
||||||
|
link: 'kubernetes.resourcePools.resourcePool',
|
||||||
|
linkParams: { id: namespace },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Applications',
|
||||||
|
link: 'kubernetes.applications',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: appName,
|
||||||
|
link: 'kubernetes.applications.application',
|
||||||
|
linkParams: { name: appName, namespace },
|
||||||
|
},
|
||||||
|
'Pods',
|
||||||
|
podID,
|
||||||
|
'Containers',
|
||||||
|
container,
|
||||||
|
'Console',
|
||||||
|
];
|
||||||
|
|
||||||
|
const disconnectConsole = useCallback(() => {
|
||||||
|
socket?.close();
|
||||||
|
terminal?.dispose();
|
||||||
|
setTerminal(null);
|
||||||
|
setSocket(null);
|
||||||
|
setConnectionStatus('closed');
|
||||||
|
}, [socket, terminal, setConnectionStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (socket) {
|
||||||
|
socket.onopen = () => {
|
||||||
|
const terminalContainer = document.getElementById('terminal-container');
|
||||||
|
if (terminalContainer) {
|
||||||
|
terminal?.open(terminalContainer);
|
||||||
|
terminal?.setOption('cursorBlink', true);
|
||||||
|
terminal?.focus();
|
||||||
|
setConnectionStatus('open');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (msg) => {
|
||||||
|
terminal?.write(msg.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = () => {
|
||||||
|
disconnectConsole();
|
||||||
|
notifyError('Websocket connection error');
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
disconnectConsole();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [disconnectConsole, setConnectionStatus, socket, terminal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
terminal?.on('data', (data) => {
|
||||||
|
socket?.send(data);
|
||||||
|
});
|
||||||
|
}, [terminal, socket]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Application console"
|
||||||
|
breadcrumbs={breadcrumbs}
|
||||||
|
reload
|
||||||
|
/>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Widget>
|
||||||
|
<WidgetBody>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12 form-section-title">Console</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row flex">
|
||||||
|
<label
|
||||||
|
htmlFor="consoleCommand"
|
||||||
|
className="col-sm-3 col-lg-2 control-label m-0 p-0 text-left"
|
||||||
|
>
|
||||||
|
Command
|
||||||
|
</label>
|
||||||
|
<div className="col-sm-8 input-group p-0">
|
||||||
|
<span className="input-group-addon">
|
||||||
|
<Icon icon={TerminalIcon} className="mr-1" />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="/bin/bash"
|
||||||
|
value={command}
|
||||||
|
onChange={(e) => setCommand(e.target.value)}
|
||||||
|
id="consoleCommand"
|
||||||
|
auto-focus="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row mt-4">
|
||||||
|
<Button
|
||||||
|
className="btn btn-primary !ml-0"
|
||||||
|
onClick={
|
||||||
|
connectionStatus === 'closed'
|
||||||
|
? connectConsole
|
||||||
|
: disconnectConsole
|
||||||
|
}
|
||||||
|
disabled={connectionStatus === 'connecting'}
|
||||||
|
>
|
||||||
|
{connectionStatus === 'open' && 'Disconnect'}
|
||||||
|
{connectionStatus === 'connecting' && 'Connecting'}
|
||||||
|
{connectionStatus !== 'connecting' &&
|
||||||
|
connectionStatus !== 'open' &&
|
||||||
|
'Connect'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</WidgetBody>
|
||||||
|
</Widget>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12 p-0">
|
||||||
|
<div id="terminal-container" className="terminal-container" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
function connectConsole() {
|
||||||
|
const params: StringDictionary = {
|
||||||
|
token: jwtToken,
|
||||||
|
endpointId: environmentId,
|
||||||
|
namespace,
|
||||||
|
podName: podID,
|
||||||
|
containerName: container,
|
||||||
|
command,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryParams = Object.keys(params)
|
||||||
|
.map((k) => `${k}=${params[k]}`)
|
||||||
|
.join('&');
|
||||||
|
|
||||||
|
let url = `${
|
||||||
|
window.location.origin
|
||||||
|
}${baseHref()}api/websocket/pod?${queryParams}`;
|
||||||
|
if (url.indexOf('https') > -1) {
|
||||||
|
url = url.replace('https://', 'wss://');
|
||||||
|
} else {
|
||||||
|
url = url.replace('http://', 'ws://');
|
||||||
|
}
|
||||||
|
|
||||||
|
setConnectionStatus('connecting');
|
||||||
|
const term = new Terminal();
|
||||||
|
setTerminal(term);
|
||||||
|
const socket = new WebSocket(url);
|
||||||
|
setSocket(socket);
|
||||||
|
}
|
||||||
|
}
|
1
app/react/kubernetes/applications/ConsoleView/index.ts
Normal file
1
app/react/kubernetes/applications/ConsoleView/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { ConsoleView } from './ConsoleView';
|
Loading…
Add table
Add a link
Reference in a new issue