mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-20 14:34:44 +02:00

Changes: Added ` xterm-addon-search ` v0.7.0 Added ` xterm-addon-search-bar ` v0.2.0 Updated ` webpack ` v4.43.0 -> v4.44.2 Updated ` xterm ` v3.14.4 -> v4.9.0 Updated ` xterm-addon-fit ` v0.1.0 -> v0.7.0 Updated ` xterm-addon-attach ` v0.1.0 -> v0.4.0 With the added packages above, when a user does Ctrl + F a search box will apear within the console for them to search whats in the console. This was requested in discord to allow the lines in the console to be searchable.
173 lines
5.9 KiB
TypeScript
173 lines
5.9 KiB
TypeScript
import React, { useEffect, useMemo, useRef } from 'react';
|
|
import { ITerminalOptions, Terminal } from 'xterm';
|
|
import { FitAddon } from 'xterm-addon-fit';
|
|
import { SearchAddon } from 'xterm-addon-search';
|
|
import { SearchBarAddon } from 'xterm-addon-search-bar';
|
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
|
import { ServerContext } from '@/state/server';
|
|
import styled from 'styled-components/macro';
|
|
import { usePermissions } from '@/plugins/usePermissions';
|
|
import tw from 'twin.macro';
|
|
import 'xterm/css/xterm.css';
|
|
import useEventListener from '@/plugins/useEventListener';
|
|
import { debounce } from 'debounce';
|
|
|
|
const theme = {
|
|
background: 'transparent',
|
|
cursor: 'transparent',
|
|
black: '#000000',
|
|
red: '#E54B4B',
|
|
green: '#9ECE58',
|
|
yellow: '#FAED70',
|
|
blue: '#396FE2',
|
|
magenta: '#BB80B3',
|
|
cyan: '#2DDAFD',
|
|
white: '#d0d0d0',
|
|
brightBlack: 'rgba(255, 255, 255, 0.2)',
|
|
brightRed: '#FF5370',
|
|
brightGreen: '#C3E88D',
|
|
brightYellow: '#FFCB6B',
|
|
brightBlue: '#82AAFF',
|
|
brightMagenta: '#C792EA',
|
|
brightCyan: '#89DDFF',
|
|
brightWhite: '#ffffff',
|
|
selection: '#FAF089',
|
|
};
|
|
|
|
const terminalProps: ITerminalOptions = {
|
|
disableStdin: true,
|
|
cursorStyle: 'underline',
|
|
allowTransparency: true,
|
|
fontSize: 12,
|
|
fontFamily: 'Menlo, Monaco, Consolas, monospace',
|
|
rows: 30,
|
|
theme: theme,
|
|
};
|
|
|
|
const TerminalDiv = styled.div`
|
|
&::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
${tw`bg-neutral-900`};
|
|
}
|
|
`;
|
|
|
|
export default () => {
|
|
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pterodactyl~ \u001b[0m';
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const terminal = useMemo(() => new Terminal({ ...terminalProps }), []);
|
|
const fitAddon = new FitAddon();
|
|
const searchAddon = new SearchAddon();
|
|
const searchAddonBar = new SearchBarAddon({ searchAddon });
|
|
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
|
const [ canSendCommands ] = usePermissions([ 'control.console' ]);
|
|
|
|
const handleConsoleOutput = (line: string, prelude = false) => terminal.writeln(
|
|
(prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
|
);
|
|
|
|
const handleDaemonErrorOutput = (line: string) => terminal.writeln(
|
|
TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
|
);
|
|
|
|
const handlePowerChangeEvent = (state: string) => terminal.writeln(
|
|
TERMINAL_PRELUDE + 'Server marked as ' + state + '...\u001b[0m',
|
|
);
|
|
|
|
const handleCommandKeydown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
if (e.key !== 'Enter' || (e.key === 'Enter' && e.currentTarget.value.length < 1)) {
|
|
return;
|
|
}
|
|
|
|
instance && instance.send('send command', e.currentTarget.value);
|
|
e.currentTarget.value = '';
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (connected && ref.current && !terminal.element) {
|
|
terminal.open(ref.current);
|
|
terminal.loadAddon(fitAddon);
|
|
terminal.loadAddon(searchAddon);
|
|
terminal.loadAddon(searchAddonBar);
|
|
fitAddon.fit();
|
|
|
|
// Add support for capturing keys
|
|
terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
|
|
// Ctrl + C ( Copy )
|
|
if (e.ctrlKey && (e.key === 'c')) {
|
|
document.execCommand('copy');
|
|
return false;
|
|
}
|
|
|
|
if (e.ctrlKey && (e.key === 'f')) {
|
|
searchAddonBar.show();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
}, [ terminal, connected ]);
|
|
|
|
const fit = debounce(() => {
|
|
fitAddon.fit();
|
|
}, 100);
|
|
|
|
useEventListener('resize', () => fit());
|
|
|
|
useEffect(() => {
|
|
if (connected && instance) {
|
|
terminal.clear();
|
|
|
|
instance.addListener('status', handlePowerChangeEvent);
|
|
instance.addListener('console output', handleConsoleOutput);
|
|
instance.addListener('install output', handleConsoleOutput);
|
|
instance.addListener('daemon message', line => handleConsoleOutput(line, true));
|
|
instance.addListener('daemon error', handleDaemonErrorOutput);
|
|
instance.send('send logs');
|
|
}
|
|
|
|
return () => {
|
|
instance && instance.removeListener('console output', handleConsoleOutput)
|
|
.removeListener('install output', handleConsoleOutput)
|
|
.removeListener('daemon message', line => handleConsoleOutput(line, true))
|
|
.removeListener('daemon error', handleDaemonErrorOutput)
|
|
.removeListener('status', handlePowerChangeEvent);
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [ connected, instance ]);
|
|
|
|
return (
|
|
<div css={tw`text-xs font-mono relative`}>
|
|
<SpinnerOverlay visible={!connected} size={'large'}/>
|
|
<div
|
|
css={[
|
|
tw`rounded-t p-2 bg-black w-full`,
|
|
!canSendCommands && tw`rounded-b`,
|
|
]}
|
|
style={{
|
|
minHeight: '16rem',
|
|
maxHeight: '32rem',
|
|
}}
|
|
>
|
|
<TerminalDiv id={'terminal'} ref={ref}/>
|
|
</div>
|
|
{canSendCommands &&
|
|
<div css={tw`rounded-b bg-neutral-900 text-neutral-100 flex`}>
|
|
<div css={tw`flex-shrink-0 p-2 font-bold`}>$</div>
|
|
<div css={tw`w-full`}>
|
|
<input
|
|
type={'text'}
|
|
disabled={!instance || !connected}
|
|
css={tw`bg-transparent text-neutral-100 p-2 pl-0 w-full`}
|
|
onKeyDown={e => handleCommandKeydown(e)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
);
|
|
};
|