From 926ca19a1ba103ea590265a5fef5a1a3d0d92216 Mon Sep 17 00:00:00 2001
From: Prabhat Khera <91852476+prabhat-org@users.noreply.github.com>
Date: Mon, 8 May 2023 14:07:46 +1200
Subject: [PATCH] feat(UI): migrate console view to react EE-2276 (#8767)
---
app/assets/css/app.css | 3 +-
app/kubernetes/__module.js | 2 +-
app/kubernetes/react/views/index.ts | 5 +
.../views/applications/console/console.html | 84 --------
.../views/applications/console/console.js | 9 -
.../applications/console/consoleController.js | 117 -----------
.../kubernetes/applications/ConsoleView/.keep | 0
.../applications/ConsoleView/ConsoleView.tsx | 197 ++++++++++++++++++
.../applications/ConsoleView/index.ts | 1 +
9 files changed, 206 insertions(+), 212 deletions(-)
delete mode 100644 app/kubernetes/views/applications/console/console.html
delete mode 100644 app/kubernetes/views/applications/console/console.js
delete mode 100644 app/kubernetes/views/applications/console/consoleController.js
delete mode 100644 app/react/kubernetes/applications/ConsoleView/.keep
create mode 100644 app/react/kubernetes/applications/ConsoleView/ConsoleView.tsx
create mode 100644 app/react/kubernetes/applications/ConsoleView/index.ts
diff --git a/app/assets/css/app.css b/app/assets/css/app.css
index aa4749814..b8bd2abb4 100644
--- a/app/assets/css/app.css
+++ b/app/assets/css/app.css
@@ -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;
}
diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js
index c235645a7..9ccdbcc8a 100644
--- a/app/kubernetes/__module.js
+++ b/app/kubernetes/__module.js
@@ -174,7 +174,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
url: '/:pod/:container/console',
views: {
'content@': {
- component: 'kubernetesApplicationConsoleView',
+ component: 'kubernetesConsoleView',
},
},
};
diff --git a/app/kubernetes/react/views/index.ts b/app/kubernetes/react/views/index.ts
index 3d5a8b8d8..59d9699ae 100644
--- a/app/kubernetes/react/views/index.ts
+++ b/app/kubernetes/react/views/index.ts
@@ -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;
diff --git a/app/kubernetes/views/applications/console/console.html b/app/kubernetes/views/applications/console/console.html
deleted file mode 100644
index 83c928c5c..000000000
--- a/app/kubernetes/views/applications/console/console.html
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
-
diff --git a/app/kubernetes/views/applications/console/console.js b/app/kubernetes/views/applications/console/console.js
deleted file mode 100644
index 780e4f0ac..000000000
--- a/app/kubernetes/views/applications/console/console.js
+++ /dev/null
@@ -1,9 +0,0 @@
-angular.module('portainer.kubernetes').component('kubernetesApplicationConsoleView', {
- templateUrl: './console.html',
- controller: 'KubernetesApplicationConsoleController',
- controllerAs: 'ctrl',
- bindings: {
- $transition$: '<',
- endpoint: '<',
- },
-});
diff --git a/app/kubernetes/views/applications/console/consoleController.js b/app/kubernetes/views/applications/console/consoleController.js
deleted file mode 100644
index f0912d106..000000000
--- a/app/kubernetes/views/applications/console/consoleController.js
+++ /dev/null
@@ -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);
diff --git a/app/react/kubernetes/applications/ConsoleView/.keep b/app/react/kubernetes/applications/ConsoleView/.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/app/react/kubernetes/applications/ConsoleView/ConsoleView.tsx b/app/react/kubernetes/applications/ConsoleView/ConsoleView.tsx
new file mode 100644
index 000000000..f31c7816f
--- /dev/null
+++ b/app/react/kubernetes/applications/ConsoleView/ConsoleView.tsx
@@ -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 (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ setCommand(e.target.value)}
+ id="consoleCommand"
+ auto-focus="true"
+ />
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+
+ 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);
+ }
+}
diff --git a/app/react/kubernetes/applications/ConsoleView/index.ts b/app/react/kubernetes/applications/ConsoleView/index.ts
new file mode 100644
index 000000000..bceeb0c68
--- /dev/null
+++ b/app/react/kubernetes/applications/ConsoleView/index.ts
@@ -0,0 +1 @@
+export { ConsoleView } from './ConsoleView';