feat(UI): migrate console view to react EE-2276 (#8767)

pull/8915/head
Prabhat Khera 2023-05-08 14:07:46 +12:00 committed by GitHub
parent c03b2ebbc1
commit 926ca19a1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 206 additions and 212 deletions

View File

@ -94,7 +94,8 @@ body,
font-size: 16px;
}
.form-horizontal .control-label.text-left {
.form-horizontal .control-label.text-left,
.form-row .control-label.text-left {
text-align: left;
font-size: 0.9em;
}

View File

@ -174,7 +174,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
url: '/:pod/:container/console',
views: {
'content@': {
component: 'kubernetesApplicationConsoleView',
component: 'kubernetesConsoleView',
},
},
};

View File

@ -8,6 +8,7 @@ import { IngressesDatatableView } from '@/react/kubernetes/ingresses/IngressData
import { CreateIngressView } from '@/react/kubernetes/ingresses/CreateIngressView';
import { DashboardView } from '@/react/kubernetes/DashboardView';
import { ServicesView } from '@/react/kubernetes/ServicesView';
import { ConsoleView } from '@/react/kubernetes/applications/ConsoleView';
export const viewsModule = angular
.module('portainer.kubernetes.react.views', [])
@ -29,4 +30,8 @@ export const viewsModule = angular
.component(
'kubernetesDashboardView',
r2a(withUIRouter(withReactQuery(withCurrentUser(DashboardView))), [])
)
.component(
'kubernetesConsoleView',
r2a(withUIRouter(withReactQuery(withCurrentUser(ConsoleView))), [])
).name;

View File

@ -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>

View File

@ -1,9 +0,0 @@
angular.module('portainer.kubernetes').component('kubernetesApplicationConsoleView', {
templateUrl: './console.html',
controller: 'KubernetesApplicationConsoleController',
controllerAs: 'ctrl',
bindings: {
$transition$: '<',
endpoint: '<',
},
});

View File

@ -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);

View 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);
}
}

View File

@ -0,0 +1 @@
export { ConsoleView } from './ConsoleView';