Merge branch 'develop' into feature/react-admin
This commit is contained in:
commit
01c03b6b77
19 changed files with 614 additions and 274 deletions
|
@ -25,6 +25,7 @@ MAIL_USERNAME=
|
||||||
MAIL_PASSWORD=
|
MAIL_PASSWORD=
|
||||||
MAIL_ENCRYPTION=tls
|
MAIL_ENCRYPTION=tls
|
||||||
MAIL_FROM=no-reply@example.com
|
MAIL_FROM=no-reply@example.com
|
||||||
|
MAILGUN_ENDPOINT=api.mailgun.net
|
||||||
# You should set this to your domain to prevent it defaulting to 'localhost', causing
|
# You should set this to your domain to prevent it defaulting to 'localhost', causing
|
||||||
# mail servers such as Gmail to reject your mail.
|
# mail servers such as Gmail to reject your mail.
|
||||||
#
|
#
|
||||||
|
|
|
@ -44,6 +44,8 @@ rules:
|
||||||
array-bracket-spacing:
|
array-bracket-spacing:
|
||||||
- warn
|
- warn
|
||||||
- always
|
- always
|
||||||
|
# Remove errors for not having newlines between operands of ternary expressions https://eslint.org/docs/rules/multiline-ternary
|
||||||
|
multiline-ternary: 0
|
||||||
"react-hooks/rules-of-hooks":
|
"react-hooks/rules-of-hooks":
|
||||||
- error
|
- error
|
||||||
"react-hooks/exhaustive-deps": 0
|
"react-hooks/exhaustive-deps": 0
|
||||||
|
@ -76,9 +78,13 @@ rules:
|
||||||
- 1
|
- 1
|
||||||
- "line-aligned"
|
- "line-aligned"
|
||||||
"react/jsx-closing-tag-location": 1
|
"react/jsx-closing-tag-location": 1
|
||||||
"no-use-before-define": 0
|
# This setup is required to avoid a spam of errors when running eslint about React being
|
||||||
"@typescript-eslint/no-use-before-define": 1
|
# used before it is defined.
|
||||||
"multiline-ternary": 0
|
#
|
||||||
|
# see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use
|
||||||
|
no-use-before-define: 0
|
||||||
|
"@typescript-eslint/no-use-before-define":
|
||||||
|
- warn
|
||||||
overrides:
|
overrides:
|
||||||
- files:
|
- files:
|
||||||
- "**/*.tsx"
|
- "**/*.tsx"
|
||||||
|
|
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -3,6 +3,28 @@ This file is a running track of new features and fixes to each version of the pa
|
||||||
|
|
||||||
This project follows [Semantic Versioning](http://semver.org) guidelines.
|
This project follows [Semantic Versioning](http://semver.org) guidelines.
|
||||||
|
|
||||||
|
## v1.4.2
|
||||||
|
### Fixed
|
||||||
|
* Fixes logic to disallow creating a backup schedule if the server's backup limit is set to 0.
|
||||||
|
* Fixes bug preventing a database host from being updated if the linked node is set to "none".
|
||||||
|
* Fixes files and menus under the "Mass Actions Bar" being unclickable when it is visible.
|
||||||
|
* Fixes issues with the Teamspeak and Mumble eggs causing installs to fail.
|
||||||
|
* Fixes automated query to avoid pruning backups that are still running unintentionally.
|
||||||
|
* Fixes "Delete Server" confirmation modal on the admin screen to actually show up when deleting rather than immediately deleting the server.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Adds support for locking individual server backups to prevent deletion by users or automated backup processes.
|
||||||
|
* List of files to be deleted is now shown on the delete file confirmation modal.
|
||||||
|
* Adds support for using `IF` statements in database queries when a database user is created through the Panel.
|
||||||
|
* Adds support for using a custom mailgun API endpoint rather than only the US based endpoint.
|
||||||
|
* Adds CPU limit display next to the current CPU usage to match disk and memory usage reporting.
|
||||||
|
* Adds a "Scroll to Bottom" helper element to the server console when not scrolled to the bottom currently.
|
||||||
|
* Adds support for querying the API for servers by using the `uuidShort` field rather than only the `uuid` field.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Updates codebase to use TypeScript 4.
|
||||||
|
* **[security]**: removes the external dependency for loading QRCode images. They're now generated directly on the frontend using JavaScript.
|
||||||
|
|
||||||
## v1.4.1
|
## v1.4.1
|
||||||
### Added
|
### Added
|
||||||
* Adds support for only running a schedule if the server is currently in an online state.
|
* Adds support for only running a schedule if the server is currently in an online state.
|
||||||
|
|
|
@ -37,6 +37,7 @@ class EmailSettingsCommand extends Command
|
||||||
{--encryption=}
|
{--encryption=}
|
||||||
{--host=}
|
{--host=}
|
||||||
{--port=}
|
{--port=}
|
||||||
|
{--endpoint=}
|
||||||
{--username=}
|
{--username=}
|
||||||
{--password=}';
|
{--password=}';
|
||||||
|
|
||||||
|
@ -140,6 +141,11 @@ class EmailSettingsCommand extends Command
|
||||||
trans('command/messages.environment.mail.ask_mailgun_secret'),
|
trans('command/messages.environment.mail.ask_mailgun_secret'),
|
||||||
$this->config->get('services.mailgun.secret')
|
$this->config->get('services.mailgun.secret')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->variables['MAILGUN_ENDPOINT'] = $this->option('endpoint') ?? $this->ask(
|
||||||
|
trans('command/messages.environment.mail.ask_mailgun_endpoint'),
|
||||||
|
$this->config->get('services.mailgun.endpoint')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -28,7 +28,7 @@ class PruneOrphanedBackupsCommand extends Command
|
||||||
|
|
||||||
$query = $repository->getBuilder()
|
$query = $repository->getBuilder()
|
||||||
->whereNull('completed_at')
|
->whereNull('completed_at')
|
||||||
->whereDate('created_at', '<=', CarbonImmutable::now()->subMinutes($since));
|
->where('created_at', '<=', CarbonImmutable::now()->subMinutes($since)->toDateTimeString());
|
||||||
|
|
||||||
$count = $query->count();
|
$count = $query->count();
|
||||||
if (!$count) {
|
if (!$count) {
|
||||||
|
|
|
@ -47,7 +47,7 @@ class ServerController extends ApplicationApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
$servers = QueryBuilder::for(Server::query())
|
$servers = QueryBuilder::for(Server::query())
|
||||||
->allowedFilters(['uuid', 'name', 'image', 'external_id'])
|
->allowedFilters(['uuid', 'uuidShort', 'name', 'image', 'external_id'])
|
||||||
->allowedSorts(['id', 'uuid', 'uuidShort', 'name', 'owner_id', 'node_id', 'status'])
|
->allowedSorts(['id', 'uuid', 'uuidShort', 'name', 'owner_id', 'node_id', 'status'])
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ class DaemonAuthenticate
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_null($bearer = $request->bearerToken())) {
|
if (is_null($bearer = $request->bearerToken())) {
|
||||||
throw new HttpException(401, 'Access this this endpoint must include an Authorization header.', null, ['WWW-Authenticate' => 'Bearer']);
|
throw new HttpException(401, 'Access to this endpoint must include an Authorization header.', null, ['WWW-Authenticate' => 'Bearer']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$parts = explode('.', $bearer);
|
$parts = explode('.', $bearer);
|
||||||
|
|
|
@ -107,7 +107,7 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
|
||||||
public function assignUserToDatabase(string $database, string $username, string $remote): bool
|
public function assignUserToDatabase(string $database, string $username, string $remote): bool
|
||||||
{
|
{
|
||||||
return $this->run(sprintf(
|
return $this->run(sprintf(
|
||||||
'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, LOCK TABLES, EXECUTE ON `%s`.* TO `%s`@`%s`',
|
'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, LOCK TABLES, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON `%s`.* TO `%s`@`%s`',
|
||||||
$database,
|
$database,
|
||||||
$username,
|
$username,
|
||||||
$remote
|
$remote
|
||||||
|
|
|
@ -16,6 +16,7 @@ return [
|
||||||
'mailgun' => [
|
'mailgun' => [
|
||||||
'domain' => env('MAILGUN_DOMAIN'),
|
'domain' => env('MAILGUN_DOMAIN'),
|
||||||
'secret' => env('MAILGUN_SECRET'),
|
'secret' => env('MAILGUN_SECRET'),
|
||||||
|
'endpoint' => env('MAILGUN_ENDPOINT')
|
||||||
],
|
],
|
||||||
|
|
||||||
'mandrill' => [
|
'mandrill' => [
|
||||||
|
|
16
package.json
16
package.json
|
@ -98,21 +98,21 @@
|
||||||
"@types/uuid": "^3.4.5",
|
"@types/uuid": "^3.4.5",
|
||||||
"@types/webpack-env": "^1.15.2",
|
"@types/webpack-env": "^1.15.2",
|
||||||
"@types/yup": "^0.29.3",
|
"@types/yup": "^0.29.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.22.1",
|
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
||||||
"@typescript-eslint/parser": "^4.22.1",
|
"@typescript-eslint/parser": "^4.25.0",
|
||||||
"autoprefixer": "^10.1.0",
|
"autoprefixer": "^10.1.0",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-styled-components": "^1.12.0",
|
"babel-plugin-styled-components": "^1.12.0",
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"css-loader": "^3.2.1",
|
"css-loader": "^3.2.1",
|
||||||
"eslint": "^7.19.0",
|
"eslint": "^7.27.0",
|
||||||
"eslint-config-standard": "^16.0.2",
|
"eslint-config-standard": "^16.0.3",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.23.3",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"eslint-plugin-react": "^7.23.2",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.1.0",
|
"fork-ts-checker-webpack-plugin": "^6.2.10",
|
||||||
"postcss": "^8.2.4",
|
"postcss": "^8.2.4",
|
||||||
"redux-devtools-extension": "^2.13.8",
|
"redux-devtools-extension": "^2.13.8",
|
||||||
"source-map-loader": "^1.0.1",
|
"source-map-loader": "^1.0.1",
|
||||||
|
|
|
@ -53,6 +53,7 @@ return [
|
||||||
'ask_smtp_username' => 'SMTP Username',
|
'ask_smtp_username' => 'SMTP Username',
|
||||||
'ask_smtp_password' => 'SMTP Password',
|
'ask_smtp_password' => 'SMTP Password',
|
||||||
'ask_mailgun_domain' => 'Mailgun Domain',
|
'ask_mailgun_domain' => 'Mailgun Domain',
|
||||||
|
'ask_mailgun_endpoint' => 'Mailgun Endpoint',
|
||||||
'ask_mailgun_secret' => 'Mailgun Secret',
|
'ask_mailgun_secret' => 'Mailgun Secret',
|
||||||
'ask_mandrill_secret' => 'Mandrill Secret',
|
'ask_mandrill_secret' => 'Mandrill Secret',
|
||||||
'ask_postmark_username' => 'Postmark API Key',
|
'ask_postmark_username' => 'Postmark API Key',
|
||||||
|
|
|
@ -76,6 +76,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
||||||
|
|
||||||
const diskLimit = server.limits.disk !== 0 ? megabytesToHuman(server.limits.disk) : 'Unlimited';
|
const diskLimit = server.limits.disk !== 0 ? megabytesToHuman(server.limits.disk) : 'Unlimited';
|
||||||
const memoryLimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
|
const memoryLimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
|
||||||
|
const cpuLimit = server.limits.cpu !== 0 ? server.limits.cpu + ' %' : 'Unlimited';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>
|
<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>
|
||||||
|
@ -130,11 +131,14 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
||||||
<Spinner size={'small'}/>
|
<Spinner size={'small'}/>
|
||||||
:
|
:
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>
|
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||||
<Icon icon={faMicrochip} $alarm={alarms.cpu}/>
|
<div css={tw`flex justify-center`}>
|
||||||
<IconDescription $alarm={alarms.cpu}>
|
<Icon icon={faMicrochip} $alarm={alarms.cpu}/>
|
||||||
{stats.cpuUsagePercent} %
|
<IconDescription $alarm={alarms.cpu}>
|
||||||
</IconDescription>
|
{stats.cpuUsagePercent.toFixed(2)} %
|
||||||
|
</IconDescription>
|
||||||
|
</div>
|
||||||
|
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {cpuLimit}</p>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||||
<div css={tw`flex justify-center`}>
|
<div css={tw`flex justify-center`}>
|
||||||
|
|
|
@ -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';
|
||||||
|
@ -73,6 +74,7 @@ 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);
|
||||||
|
@ -140,6 +142,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();
|
||||||
|
@ -201,7 +204,7 @@ export default () => {
|
||||||
|
|
||||||
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`,
|
||||||
|
@ -209,21 +212,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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -74,6 +74,7 @@ const ServerDetailsBlock = () => {
|
||||||
|
|
||||||
const diskLimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
const diskLimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
||||||
const memoryLimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
const memoryLimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
||||||
|
const cpuLimit = limits.cpu ? limits.cpu + '%' : 'Unlimited';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TitledGreyBox css={tw`break-words`} title={name} icon={faServer}>
|
<TitledGreyBox css={tw`break-words`} title={name} icon={faServer}>
|
||||||
|
@ -96,6 +97,7 @@ const ServerDetailsBlock = () => {
|
||||||
</CopyOnClick>
|
</CopyOnClick>
|
||||||
<p css={tw`text-xs mt-2`}>
|
<p css={tw`text-xs mt-2`}>
|
||||||
<FontAwesomeIcon icon={faMicrochip} fixedWidth css={tw`mr-1`}/> {stats.cpu.toFixed(2)}%
|
<FontAwesomeIcon icon={faMicrochip} fixedWidth css={tw`mr-1`}/> {stats.cpu.toFixed(2)}%
|
||||||
|
<span css={tw`text-neutral-500`}> / {cpuLimit}</span>
|
||||||
</p>
|
</p>
|
||||||
<p css={tw`text-xs mt-2`}>
|
<p css={tw`text-xs mt-2`}>
|
||||||
<FontAwesomeIcon icon={faMemory} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.memory)}
|
<FontAwesomeIcon icon={faMemory} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.memory)}
|
||||||
|
|
|
@ -96,7 +96,7 @@ export default () => {
|
||||||
|
|
||||||
setCpu(
|
setCpu(
|
||||||
new Chart(node.getContext('2d')!, chartDefaults({
|
new Chart(node.getContext('2d')!, chartDefaults({
|
||||||
callback: (value) => `${value}%`,
|
callback: (value) => `${value}% `,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -62,7 +62,7 @@ export default () => {
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<Can action={'database.create'}>
|
<Can action={'database.create'}>
|
||||||
<div css={tw`mt-6 sm:flex items-center justify-end`}>
|
<div css={tw`mt-6 flex items-center justify-end`}>
|
||||||
{(databaseLimit > 0 && databases.length > 0) &&
|
{(databaseLimit > 0 && databases.length > 0) &&
|
||||||
<p css={tw`text-sm text-neutral-300 mb-4 sm:mr-6 sm:mb-0`}>
|
<p css={tw`text-sm text-neutral-300 mb-4 sm:mr-6 sm:mb-0`}>
|
||||||
{databases.length} of {databaseLimit} databases have been allocated to this
|
{databases.length} of {databaseLimit} databases have been allocated to this
|
||||||
|
@ -70,7 +70,7 @@ export default () => {
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
{databaseLimit > 0 && databaseLimit !== databases.length &&
|
{databaseLimit > 0 && databaseLimit !== databases.length &&
|
||||||
<CreateDatabaseButton css={tw`w-full sm:w-auto`}/>
|
<CreateDatabaseButton css={tw`flex justify-end mt-6`}/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Can>
|
</Can>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PortaledModal, { ModalProps } from '@/components/elements/Modal';
|
import PortaledModal, { ModalProps } from '@/components/elements/Modal';
|
||||||
import ModalContext, { ModalContextValues } from '@/context/ModalContext';
|
import ModalContext, { ModalContextValues } from '@/context/ModalContext';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
|
||||||
export interface AsModalProps {
|
export interface AsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -47,13 +48,13 @@ function asModal<P extends {}> (modalProps?: SettableModalProps | ((props: P) =>
|
||||||
/**
|
/**
|
||||||
* @this {React.PureComponent<P & AsModalProps, State>}
|
* @this {React.PureComponent<P & AsModalProps, State>}
|
||||||
*/
|
*/
|
||||||
componentDidUpdate (prevProps: Readonly<P & AsModalProps>) {
|
componentDidUpdate (prevProps: Readonly<P & AsModalProps>, prevState: Readonly<State>) {
|
||||||
if (prevProps.visible && !this.props.visible) {
|
if (prevProps.visible && !this.props.visible) {
|
||||||
this.setState({ visible: false, showSpinnerOverlay: false });
|
this.setState({ visible: false, showSpinnerOverlay: false });
|
||||||
} else if (!prevProps.visible && this.props.visible) {
|
} else if (!prevProps.visible && this.props.visible) {
|
||||||
this.setState({ render: true, visible: true });
|
this.setState({ render: true, visible: true });
|
||||||
}
|
}
|
||||||
if (!this.state.render) {
|
if (!this.state.render && !isEqual(prevState.propOverrides, this.state.propOverrides)) {
|
||||||
this.setState({ propOverrides: {} });
|
this.setState({ propOverrides: {} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue