import { Terminal } from 'xterm'; import { fit } from 'xterm/lib/addons/fit/fit'; import { useCallback, useEffect, useRef, useState } from 'react'; import clsx from 'clsx'; import { RotateCw, X, Terminal as TerminalIcon } from 'lucide-react'; import { baseHref } from '@/portainer/helpers/pathHelper'; import { terminalClose, terminalResize, } from '@/portainer/services/terminal-window'; import { EnvironmentId } from '@/react/portainer/environments/types'; import { error as notifyError } from '@/portainer/services/notifications'; import { Icon } from '@@/Icon'; import { Button } from '@@/buttons'; import styles from './KubectlShell.module.css'; interface ShellState { socket: WebSocket | null; minimized: boolean; } interface Props { environmentId: EnvironmentId; onClose(): void; } export function KubeCtlShell({ environmentId, onClose }: Props) { const [terminal] = useState(new Terminal()); const [shell, setShell] = useState({ socket: null, minimized: false, }); const { socket } = shell; const terminalElem = useRef(null); const handleClose = useCallback(() => { terminalClose(); // only css trick socket?.close(); terminal.dispose(); onClose(); }, [onClose, terminal, socket]); const openTerminal = useCallback(() => { if (!terminalElem.current) { return; } terminal.open(terminalElem.current); terminal.setOption('cursorBlink', true); terminal.focus(); fit(terminal); terminal.writeln('#Run kubectl commands inside here'); terminal.writeln('#e.g. kubectl get all'); terminal.writeln(''); }, [terminal]); // refresh socket listeners on socket updates useEffect(() => { if (!socket) { return () => {}; } function onOpen() { openTerminal(); } function onMessage(e: MessageEvent) { const encoded = new TextEncoder().encode(e.data); terminal.writeUtf8(encoded); } function onClose() { handleClose(); } function onError(e: Event) { handleClose(); if (socket?.readyState !== WebSocket.CLOSED) { notifyError( 'Failure', e as unknown as Error, 'Websocket connection error' ); } } socket.addEventListener('open', onOpen); socket.addEventListener('message', onMessage); socket.addEventListener('close', onClose); socket.addEventListener('error', onError); return () => { socket.removeEventListener('open', onOpen); socket.removeEventListener('message', onMessage); socket.removeEventListener('close', onClose); socket.removeEventListener('error', onError); }; }, [handleClose, openTerminal, socket, terminal]); // on component load/destroy useEffect(() => { const socket = new WebSocket(buildUrl(environmentId)); setShell((shell) => ({ ...shell, socket })); terminal.onData((data) => socket.send(data)); terminal.onKey(({ domEvent }) => { if (domEvent.ctrlKey && domEvent.code === 'KeyD') { close(); } }); window.addEventListener('resize', () => terminalResize()); function close() { socket.close(); terminal.dispose(); window.removeEventListener('resize', terminalResize); } return close; }, [environmentId, terminal]); return (
kubectl shell
Loading Terminal...
); function clearScreen() { terminal.clear(); } function toggleMinimize() { if (shell.minimized) { terminalResize(); setShell((shell) => ({ ...shell, minimized: false })); } else { terminalClose(); setShell((shell) => ({ ...shell, minimized: true })); } } function buildUrl(environmentId: EnvironmentId) { const params = { endpointId: environmentId, }; const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; const path = `${baseHref()}api/websocket/kubernetes-shell`; const base = path.startsWith('http') ? path.replace(/^https?:\/\//i, '') : window.location.host + path; const queryParams = Object.entries(params) .map(([k, v]) => `${k}=${v}`) .join('&'); return `${wsProtocol}${base}?${queryParams}`; } }