Merge branch 'develop' into feature/react-admin

This commit is contained in:
Matthew Penner 2021-06-06 13:54:04 -06:00
commit 01c03b6b77
19 changed files with 614 additions and 274 deletions

View file

@ -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.
# #

View file

@ -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"

View file

@ -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.

View file

@ -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')
);
} }
/** /**

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

@ -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' => [

View file

@ -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",

View file

@ -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',

View file

@ -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`}>

View file

@ -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>
); );

View file

@ -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)}

View file

@ -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}% `,
})), })),
); );
}, []); }, []);

View file

@ -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>

View file

@ -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: {} });
} }
} }

View 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;
}
}

695
yarn.lock

File diff suppressed because it is too large Load diff