mirror of https://github.com/portainer/portainer
				
				
				
			feat(UI): migrate console view to react EE-2276 (#8767)
							parent
							
								
									c03b2ebbc1
								
							
						
					
					
						commit
						926ca19a1b
					
				| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -174,7 +174,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
 | 
			
		|||
      url: '/:pod/:container/console',
 | 
			
		||||
      views: {
 | 
			
		||||
        'content@': {
 | 
			
		||||
          component: 'kubernetesApplicationConsoleView',
 | 
			
		||||
          component: 'kubernetesConsoleView',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export { ConsoleView } from './ConsoleView';
 | 
			
		||||
		Loading…
	
		Reference in New Issue