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 @@ - - - - - -
-
-
- - -
-
Console
- -
- -
- - - - -
-
- -
-
- - -
-
-
-
-
-
-
- -
-
-
-
-
-
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 ( + <> + +
+
+ + +
+
Console
+
+
+ +
+ + + + 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';