add scroll down helper to console (#2951)
This commit is contained in:
parent
75d254a6a4
commit
647d2cbf92
4 changed files with 96 additions and 25 deletions
|
@ -37,7 +37,7 @@
|
||||||
"swr": "^0.2.3",
|
"swr": "^0.2.3",
|
||||||
"tailwindcss": "^2.0.2",
|
"tailwindcss": "^2.0.2",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
"xterm": "^4.9.0",
|
"xterm": "^4.12.0",
|
||||||
"xterm-addon-attach": "^0.6.0",
|
"xterm-addon-attach": "^0.6.0",
|
||||||
"xterm-addon-fit": "^0.4.0",
|
"xterm-addon-fit": "^0.4.0",
|
||||||
"xterm-addon-search": "^0.7.0",
|
"xterm-addon-search": "^0.7.0",
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { ITerminalOptions, Terminal } from 'xterm';
|
import { Terminal, ITerminalOptions } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
import { SearchAddon } from 'xterm-addon-search';
|
import { SearchAddon } from 'xterm-addon-search';
|
||||||
import { SearchBarAddon } from 'xterm-addon-search-bar';
|
import { SearchBarAddon } from 'xterm-addon-search-bar';
|
||||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||||
|
import { ScrollDownHelperAddon } from '@/plugins/XtermScrollDownHelperAddon';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import styled from 'styled-components/macro';
|
import styled from 'styled-components/macro';
|
||||||
|
@ -72,12 +73,13 @@ export default () => {
|
||||||
const searchAddon = new SearchAddon();
|
const searchAddon = new SearchAddon();
|
||||||
const searchBar = new SearchBarAddon({ searchAddon });
|
const searchBar = new SearchBarAddon({ searchAddon });
|
||||||
const webLinksAddon = new WebLinksAddon();
|
const webLinksAddon = new WebLinksAddon();
|
||||||
|
const scrollDownHelperAddon = new ScrollDownHelperAddon();
|
||||||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||||
const [ canSendCommands ] = usePermissions([ 'control.console' ]);
|
const [canSendCommands] = usePermissions(['control.console']);
|
||||||
const serverId = ServerContext.useStoreState(state => state.server.data!.id);
|
const serverId = ServerContext.useStoreState(state => state.server.data!.id);
|
||||||
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
|
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
|
||||||
const [ history, setHistory ] = usePersistedState<string[]>(`${serverId}:command_history`, []);
|
const [history, setHistory] = usePersistedState<string[]>(`${serverId}:command_history`, []);
|
||||||
const [ historyIndex, setHistoryIndex ] = useState(-1);
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||||
|
|
||||||
const handleConsoleOutput = (line: string, prelude = false) => terminal.writeln(
|
const handleConsoleOutput = (line: string, prelude = false) => terminal.writeln(
|
||||||
(prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
(prelude ? TERMINAL_PRELUDE : '') + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
|
||||||
|
@ -125,7 +127,7 @@ export default () => {
|
||||||
|
|
||||||
const command = e.currentTarget.value;
|
const command = e.currentTarget.value;
|
||||||
if (e.key === 'Enter' && command.length > 0) {
|
if (e.key === 'Enter' && command.length > 0) {
|
||||||
setHistory(prevHistory => [ command, ...prevHistory! ].slice(0, 32));
|
setHistory(prevHistory => [command, ...prevHistory!].slice(0, 32));
|
||||||
setHistoryIndex(-1);
|
setHistoryIndex(-1);
|
||||||
|
|
||||||
instance && instance.send('send command', command);
|
instance && instance.send('send command', command);
|
||||||
|
@ -139,6 +141,7 @@ export default () => {
|
||||||
terminal.loadAddon(searchAddon);
|
terminal.loadAddon(searchAddon);
|
||||||
terminal.loadAddon(searchBar);
|
terminal.loadAddon(searchBar);
|
||||||
terminal.loadAddon(webLinksAddon);
|
terminal.loadAddon(webLinksAddon);
|
||||||
|
terminal.loadAddon(scrollDownHelperAddon);
|
||||||
|
|
||||||
terminal.open(ref.current);
|
terminal.open(ref.current);
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
|
@ -158,7 +161,7 @@ export default () => {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [ terminal, connected ]);
|
}, [terminal, connected]);
|
||||||
|
|
||||||
useEventListener('resize', debounce(() => {
|
useEventListener('resize', debounce(() => {
|
||||||
if (terminal.element) {
|
if (terminal.element) {
|
||||||
|
@ -196,11 +199,11 @@ export default () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [ connected, instance ]);
|
}, [connected, instance]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div css={tw`text-xs font-mono relative`}>
|
<div css={tw`text-xs font-mono relative`}>
|
||||||
<SpinnerOverlay visible={!connected} size={'large'}/>
|
<SpinnerOverlay visible={!connected} size={'large'} />
|
||||||
<div
|
<div
|
||||||
css={[
|
css={[
|
||||||
tw`rounded-t p-2 bg-black w-full`,
|
tw`rounded-t p-2 bg-black w-full`,
|
||||||
|
@ -208,21 +211,21 @@ export default () => {
|
||||||
]}
|
]}
|
||||||
style={{ minHeight: '16rem' }}
|
style={{ minHeight: '16rem' }}
|
||||||
>
|
>
|
||||||
<TerminalDiv id={'terminal'} ref={ref}/>
|
<TerminalDiv id={'terminal'} ref={ref} />
|
||||||
</div>
|
</div>
|
||||||
{canSendCommands &&
|
{canSendCommands &&
|
||||||
<div css={tw`rounded-b bg-neutral-900 text-neutral-100 flex items-baseline`}>
|
<div css={tw`rounded-b bg-neutral-900 text-neutral-100 flex items-baseline`}>
|
||||||
<div css={tw`flex-shrink-0 p-2 font-bold`}>$</div>
|
<div css={tw`flex-shrink-0 p-2 font-bold`}>$</div>
|
||||||
<div css={tw`w-full`}>
|
<div css={tw`w-full`}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
type={'text'}
|
type={'text'}
|
||||||
placeholder={'Type a command...'}
|
placeholder={'Type a command...'}
|
||||||
aria-label={'Console command input.'}
|
aria-label={'Console command input.'}
|
||||||
disabled={!instance || !connected}
|
disabled={!instance || !connected}
|
||||||
onKeyDown={handleCommandKeyDown}
|
onKeyDown={handleCommandKeyDown}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
68
resources/scripts/plugins/XtermScrollDownHelperAddon.ts
Normal file
68
resources/scripts/plugins/XtermScrollDownHelperAddon.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { Terminal, ITerminalAddon } from 'xterm';
|
||||||
|
|
||||||
|
export class ScrollDownHelperAddon implements ITerminalAddon {
|
||||||
|
private terminal: Terminal = new Terminal();
|
||||||
|
private element?: HTMLDivElement;
|
||||||
|
|
||||||
|
activate (terminal: Terminal): void {
|
||||||
|
this.terminal = terminal;
|
||||||
|
|
||||||
|
this.terminal.onScroll(() => {
|
||||||
|
if (this.isScrolledDown()) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.terminal.onLineFeed(() => {
|
||||||
|
if (!this.isScrolledDown()) {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose (): void {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
show (): void {
|
||||||
|
if (!this.terminal || !this.terminal.element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.element) {
|
||||||
|
this.element.style.visibility = 'visible';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.terminal.element.style.position = 'relative';
|
||||||
|
|
||||||
|
this.element = document.createElement('div');
|
||||||
|
this.element.innerHTML = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="bell" class="svg-inline--fa fa-bell fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64zm215.39-149.71c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71z"></path></svg>';
|
||||||
|
this.element.style.position = 'absolute';
|
||||||
|
this.element.style.right = '1.5rem';
|
||||||
|
this.element.style.bottom = '.5rem';
|
||||||
|
this.element.style.padding = '.5rem';
|
||||||
|
this.element.style.fontSize = '1.25em';
|
||||||
|
this.element.style.boxShadow = '0 2px 8px #000';
|
||||||
|
this.element.style.backgroundColor = '#252526';
|
||||||
|
this.element.style.zIndex = '999';
|
||||||
|
this.element.style.cursor = 'pointer';
|
||||||
|
|
||||||
|
this.element.addEventListener('click', () => {
|
||||||
|
this.terminal.scrollToBottom();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.terminal.element.appendChild(this.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
hide (): void {
|
||||||
|
if (this.element) {
|
||||||
|
this.element.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isScrolledDown (): boolean {
|
||||||
|
return this.terminal.buffer.active.viewportY === this.terminal.buffer.active.baseY;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8040,10 +8040,10 @@ xterm-addon-web-links@^0.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0.tgz#265cbf8221b9b315d0a748e1323bee331cd5da03"
|
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0.tgz#265cbf8221b9b315d0a748e1323bee331cd5da03"
|
||||||
integrity sha512-xv8GeiINmx0zENO9hf5k+5bnkaE8mRzF+OBAr9WeFq2eLaQSudioQSiT34M1ofKbzcdjSsKiZm19Rw3i4eXamg==
|
integrity sha512-xv8GeiINmx0zENO9hf5k+5bnkaE8mRzF+OBAr9WeFq2eLaQSudioQSiT34M1ofKbzcdjSsKiZm19Rw3i4eXamg==
|
||||||
|
|
||||||
xterm@^4.9.0:
|
xterm@^4.12.0:
|
||||||
version "4.9.0"
|
version "4.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.9.0.tgz#7a4c097a433d565339b5533b468bbc60c6c87969"
|
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.12.0.tgz#db09b425b4dcae5b96f8cbbaaa93b3bc60997ca9"
|
||||||
integrity sha512-wGfqufmioctKr8VkbRuZbVDfjlXWGZZ1PWHy1yqqpGT3Nm6yaJx8lxDbSEBANtgaiVPTcKSp97sxOy5IlpqYfw==
|
integrity sha512-K5mF/p3txUV18mjiZFlElagoHFpqXrm5OYHeoymeXSu8GG/nMaOO/+NRcNCwfdjzAbdQ5VLF32hEHiWWKKm0bw==
|
||||||
|
|
||||||
y18n@^4.0.0:
|
y18n@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
|
|
Loading…
Reference in a new issue