commit
d3a544ac5d
52 changed files with 498 additions and 239 deletions
23
README.md
23
README.md
|
@ -12,6 +12,27 @@ What more are you waiting for? Make game servers a first class citizen on your p
|
||||||
|
|
||||||
![Image](https://cdn.pterodactyl.io/site-assets/mockup-macbook-grey.png)
|
![Image](https://cdn.pterodactyl.io/site-assets/mockup-macbook-grey.png)
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
I would like to extend my sincere thanks to the following sponsors for funding Pterodactyl's developement. [Interested
|
||||||
|
in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
|
||||||
|
|
||||||
|
#### [BloomVPS](https://bloomvps.com)
|
||||||
|
> BloomVPS offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly
|
||||||
|
> unbeatable prices on high-performance hosting.
|
||||||
|
|
||||||
|
#### [VersatileNode](https://versatilenode.com/)
|
||||||
|
> Looking to host a minecraft server, vps, or a website? VersatileNode is one of the most affordable hosting providers
|
||||||
|
> to provide quality yet cheap services with incredible support.
|
||||||
|
|
||||||
|
#### [MineStrator](https://minestrator.com/)
|
||||||
|
> Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord
|
||||||
|
> trust us.
|
||||||
|
|
||||||
|
#### [DedicatedMC](https://dedicatedmc.io/)
|
||||||
|
> DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance
|
||||||
|
> and giving you the best performance money can buy.
|
||||||
|
|
||||||
|
|
||||||
## Support & Documentation
|
## Support & Documentation
|
||||||
Support for using Pterodactyl can be found on our [Documentation Website](https://pterodactyl.io/project/introduction.html), [Guides Website](https://pterodactyl.io/community/about.html), or via our [Discord Chat](https://discord.gg/QRDZvVm).
|
Support for using Pterodactyl can be found on our [Documentation Website](https://pterodactyl.io/project/introduction.html), [Guides Website](https://pterodactyl.io/community/about.html), or via our [Discord Chat](https://discord.gg/QRDZvVm).
|
||||||
|
|
||||||
|
@ -43,7 +64,7 @@ In addition to our standard nest of supported games, our community is constantly
|
||||||
## Credits
|
## Credits
|
||||||
This software would not be possible without the work of other open-source authors who provide tools such as:
|
This software would not be possible without the work of other open-source authors who provide tools such as:
|
||||||
|
|
||||||
[Ace Editor](https://ace.c9.io), [AdminLTE](https://almsaeedstudio.com), [Animate.css](http://daneden.github.io/animate.css/), [AnsiUp](https://github.com/drudru/ansi_up), [Async.js](https://github.com/caolan/async),
|
[Ace Editor](https://ace.c9.io), [AdminLTE](https://adminlte.io), [Animate.css](http://daneden.github.io/animate.css/), [AnsiUp](https://github.com/drudru/ansi_up), [Async.js](https://github.com/caolan/async),
|
||||||
[Bootstrap](http://getbootstrap.com), [Bootstrap Notify](http://bootstrap-notify.remabledesigns.com), [Chart.js](http://www.chartjs.org), [FontAwesome](http://fontawesome.io),
|
[Bootstrap](http://getbootstrap.com), [Bootstrap Notify](http://bootstrap-notify.remabledesigns.com), [Chart.js](http://www.chartjs.org), [FontAwesome](http://fontawesome.io),
|
||||||
[FontAwesome Animations](https://github.com/l-lin/font-awesome-animation), [jQuery](http://jquery.com), [Laravel](https://laravel.com), [Lodash](https://lodash.com),
|
[FontAwesome Animations](https://github.com/l-lin/font-awesome-animation), [jQuery](http://jquery.com), [Laravel](https://laravel.com), [Lodash](https://lodash.com),
|
||||||
[Select2](https://select2.github.io), [Socket.io](http://socket.io), [Socket.io File Upload](https://github.com/vote539/socketio-file-upload), [SweetAlert](http://t4t5.github.io/sweetalert),
|
[Select2](https://select2.github.io), [Socket.io](http://socket.io), [Socket.io File Upload](https://github.com/vote539/socketio-file-upload), [SweetAlert](http://t4t5.github.io/sweetalert),
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Pterodactyl\Exceptions\Http\Connection;
|
namespace Pterodactyl\Exceptions\Http\Connection;
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use Pterodactyl\Exceptions\DisplayException;
|
use Pterodactyl\Exceptions\DisplayException;
|
||||||
|
@ -28,12 +29,28 @@ class DaemonConnectionException extends DisplayException
|
||||||
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;
|
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;
|
||||||
|
|
||||||
if ($useStatusCode) {
|
if ($useStatusCode) {
|
||||||
$this->statusCode = is_null($response) ? 500 : $response->getStatusCode();
|
$this->statusCode = is_null($response) ? $this->statusCode : $response->getStatusCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
parent::__construct(trans('admin/server.exceptions.daemon_exception', [
|
$message = trans('admin/server.exceptions.daemon_exception', [
|
||||||
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
|
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
|
||||||
]), $previous, DisplayException::LEVEL_WARNING);
|
]);
|
||||||
|
|
||||||
|
// Attempt to pull the actual error message off the response and return that if it is not
|
||||||
|
// a 500 level error.
|
||||||
|
if ($this->statusCode < 500 && ! is_null($response)) {
|
||||||
|
$body = $response->getBody();
|
||||||
|
if (is_string($body) || (is_object($body) && method_exists($body, '__toString'))) {
|
||||||
|
$body = json_decode(is_string($body) ? $body : $body->__toString(), true);
|
||||||
|
$message = "[Wings Error]: " . Arr::get($body, 'error', $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$level = $this->statusCode >= 500 && $this->statusCode !== 504
|
||||||
|
? DisplayException::LEVEL_ERROR
|
||||||
|
: DisplayException::LEVEL_WARNING;
|
||||||
|
|
||||||
|
parent::__construct($message, $previous, $level);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,6 +19,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
|
||||||
'app:name' => 'required|string|max:255',
|
'app:name' => 'required|string|max:255',
|
||||||
'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2',
|
'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2',
|
||||||
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
|
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
|
||||||
|
'app:analytics' => 'nullable|string',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
|
||||||
'app:name' => 'Company Name',
|
'app:name' => 'Company Name',
|
||||||
'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication',
|
'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication',
|
||||||
'app:locale' => 'Default Language',
|
'app:locale' => 'Default Language',
|
||||||
|
'app:analytics' => 'Google Analytics',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ class AssetComposer
|
||||||
'enabled' => config('recaptcha.enabled', false),
|
'enabled' => config('recaptcha.enabled', false),
|
||||||
'siteKey' => config('recaptcha.website_key') ?? '',
|
'siteKey' => config('recaptcha.website_key') ?? '',
|
||||||
],
|
],
|
||||||
|
'analytics' => config('app.analytics') ?? '',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ class SettingsServiceProvider extends ServiceProvider
|
||||||
protected $keys = [
|
protected $keys = [
|
||||||
'app:name',
|
'app:name',
|
||||||
'app:locale',
|
'app:locale',
|
||||||
|
'app:analytics',
|
||||||
'recaptcha:enabled',
|
'recaptcha:enabled',
|
||||||
'recaptcha:secret_key',
|
'recaptcha:secret_key',
|
||||||
'recaptcha:website_key',
|
'recaptcha:website_key',
|
||||||
|
|
|
@ -230,6 +230,9 @@ class DaemonFileRepository extends DaemonRepository
|
||||||
'root' => $root ?? '/',
|
'root' => $root ?? '/',
|
||||||
'files' => $files,
|
'files' => $files,
|
||||||
],
|
],
|
||||||
|
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
|
||||||
|
// since it will likely take quite awhile for large directories.
|
||||||
|
'timeout' => 60 * 15,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch (TransferException $exception) {
|
} catch (TransferException $exception) {
|
||||||
|
|
|
@ -51,12 +51,29 @@ class EggConfigurationService
|
||||||
);
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'startup' => json_decode($server->egg->inherit_config_startup),
|
'startup' => $this->convertStartupToNewFormat(json_decode($server->egg->inherit_config_startup, true)),
|
||||||
'stop' => $this->convertStopToNewFormat($server->egg->inherit_config_stop),
|
'stop' => $this->convertStopToNewFormat($server->egg->inherit_config_stop),
|
||||||
'configs' => $configs,
|
'configs' => $configs,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the "done" variable into an array if it is not currently one.
|
||||||
|
*
|
||||||
|
* @param array $startup
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function convertStartupToNewFormat(array $startup)
|
||||||
|
{
|
||||||
|
$done = Arr::get($startup, 'done');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'done' => is_string($done) ? [$done] : $done,
|
||||||
|
'user_interaction' => Arr::get($startup, 'userInteraction') ?? Arr::get($startup, 'user_interaction') ?? [],
|
||||||
|
'strip_ansi' => Arr::get($startup, 'strip_ansi') ?? false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a legacy stop string into a new generation stop option for a server.
|
* Converts a legacy stop string into a new generation stop option for a server.
|
||||||
*
|
*
|
||||||
|
|
|
@ -27,11 +27,11 @@ class StatsTransformer extends BaseClientTransformer
|
||||||
'current_state' => Arr::get($data, 'state', 'stopped'),
|
'current_state' => Arr::get($data, 'state', 'stopped'),
|
||||||
'is_suspended' => Arr::get($data, 'suspended', false),
|
'is_suspended' => Arr::get($data, 'suspended', false),
|
||||||
'resources' => [
|
'resources' => [
|
||||||
'memory_bytes' => Arr::get($data, 'resources.memory_bytes', 0),
|
'memory_bytes' => Arr::get($data, 'memory_bytes', 0),
|
||||||
'cpu_absolute' => Arr::get($data, 'resources.cpu_absolute', 0),
|
'cpu_absolute' => Arr::get($data, 'cpu_absolute', 0),
|
||||||
'disk_bytes' => Arr::get($data, 'resources.disk_bytes', 0),
|
'disk_bytes' => Arr::get($data, 'disk_bytes', 0),
|
||||||
'network_rx_bytes' => Arr::get($data, 'resources.network.rx_bytes', 0),
|
'network_rx_bytes' => Arr::get($data, 'network.rx_bytes', 0),
|
||||||
'network_tx_bytes' => Arr::get($data, 'resources.network.tx_bytes', 0),
|
'network_tx_bytes' => Arr::get($data, 'network.tx_bytes', 0),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,8 +85,8 @@ return [
|
||||||
| Configure the timeout to be used for Guzzle connections here.
|
| Configure the timeout to be used for Guzzle connections here.
|
||||||
*/
|
*/
|
||||||
'guzzle' => [
|
'guzzle' => [
|
||||||
'timeout' => env('GUZZLE_TIMEOUT', 5),
|
'timeout' => env('GUZZLE_TIMEOUT', 30),
|
||||||
'connect_timeout' => env('GUZZLE_CONNECT_TIMEOUT', 3),
|
'connect_timeout' => env('GUZZLE_CONNECT_TIMEOUT', 10),
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.19",
|
"@fortawesome/fontawesome-svg-core": "1.2.19",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.9.0",
|
"@fortawesome/free-solid-svg-icons": "^5.9.0",
|
||||||
"@fortawesome/react-fontawesome": "0.1.4",
|
"@fortawesome/react-fontawesome": "0.1.4",
|
||||||
"@types/react-google-recaptcha": "^1.1.1",
|
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"ayu-ace": "^2.0.4",
|
"ayu-ace": "^2.0.4",
|
||||||
"brace": "^0.11.1",
|
"brace": "^0.11.1",
|
||||||
|
@ -26,11 +25,14 @@
|
||||||
"react-dom": "npm:@hot-loader/react-dom",
|
"react-dom": "npm:@hot-loader/react-dom",
|
||||||
"react-fast-compare": "^3.2.0",
|
"react-fast-compare": "^3.2.0",
|
||||||
"react-google-recaptcha": "^2.0.1",
|
"react-google-recaptcha": "^2.0.1",
|
||||||
|
"react-helmet": "^6.1.0",
|
||||||
|
"react-ga": "^3.1.2",
|
||||||
"react-hot-loader": "^4.12.21",
|
"react-hot-loader": "^4.12.21",
|
||||||
"react-i18next": "^11.2.1",
|
"react-i18next": "^11.2.1",
|
||||||
"react-redux": "^7.1.0",
|
"react-redux": "^7.1.0",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
|
"reaptcha": "^1.7.2",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"styled-components": "^5.1.1",
|
"styled-components": "^5.1.1",
|
||||||
"styled-components-breakpoint": "^3.0.0-preview.20",
|
"styled-components-breakpoint": "^3.0.0-preview.20",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"@types/query-string": "^6.3.0",
|
"@types/query-string": "^6.3.0",
|
||||||
"@types/react": "^16.9.41",
|
"@types/react": "^16.9.41",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@types/react-dom": "^16.9.8",
|
||||||
|
"@types/react-helmet": "^6.0.0",
|
||||||
"@types/react-redux": "^7.1.1",
|
"@types/react-redux": "^7.1.1",
|
||||||
"@types/react-router": "^5.1.3",
|
"@types/react-router": "^5.1.3",
|
||||||
"@types/react-router-dom": "^5.1.3",
|
"@types/react-router-dom": "^5.1.3",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
export default (email: string): Promise<string> => {
|
export default (email: string, recaptchaData?: string): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/auth/password', { email })
|
http.post('/auth/password', { email, 'g-recaptcha-response': recaptchaData })
|
||||||
.then(response => resolve(response.data.status || ''))
|
.then(response => resolve(response.data.status || ''))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { rawDataToFileObject } from '@/api/transformers';
|
||||||
|
|
||||||
export default async (uuid: string, directory: string, files: string[]): Promise<FileObject> => {
|
export default async (uuid: string, directory: string, files: string[]): Promise<FileObject> => {
|
||||||
const { data } = await http.post(`/api/client/servers/${uuid}/files/compress`, { root: directory, files }, {
|
const { data } = await http.post(`/api/client/servers/${uuid}/files/compress`, { root: directory, files }, {
|
||||||
timeout: 300000,
|
timeout: 60000,
|
||||||
timeoutErrorMessage: 'It looks like this archive is taking a long time to generate. It will appear when completed.',
|
timeoutErrorMessage: 'It looks like this archive is taking a long time to generate. It will appear once completed.',
|
||||||
});
|
});
|
||||||
|
|
||||||
return rawDataToFileObject(data);
|
return rawDataToFileObject(data);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import http from '@/api/http';
|
||||||
import { rawDataToFileObject } from '@/api/transformers';
|
import { rawDataToFileObject } from '@/api/transformers';
|
||||||
|
|
||||||
export interface FileObject {
|
export interface FileObject {
|
||||||
uuid: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Allocation } from '@/api/server/getServer';
|
import { Allocation } from '@/api/server/getServer';
|
||||||
import { FractalResponseData } from '@/api/http';
|
import { FractalResponseData } from '@/api/http';
|
||||||
import { FileObject } from '@/api/server/files/loadDirectory';
|
import { FileObject } from '@/api/server/files/loadDirectory';
|
||||||
import v4 from 'uuid/v4';
|
|
||||||
|
|
||||||
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
|
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
|
||||||
id: data.attributes.id,
|
id: data.attributes.id,
|
||||||
|
@ -13,7 +12,7 @@ export const rawDataToServerAllocation = (data: FractalResponseData): Allocation
|
||||||
});
|
});
|
||||||
|
|
||||||
export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
|
export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
|
||||||
uuid: v4(),
|
key: `${data.attributes.is_file ? 'file' : 'dir'}_${data.attributes.name}`,
|
||||||
name: data.attributes.name,
|
name: data.attributes.name,
|
||||||
mode: data.attributes.mode,
|
mode: data.attributes.mode,
|
||||||
size: Number(data.attributes.size),
|
size: Number(data.attributes.size),
|
||||||
|
|
|
@ -32,4 +32,41 @@ export default createGlobalStyle`
|
||||||
input[type=number] {
|
input[type=number] {
|
||||||
-moz-appearance: textfield !important;
|
-moz-appearance: textfield !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scroll Bar Style */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
background: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border: solid 0 rgb(0 0 0 / 0%);
|
||||||
|
border-right-width: 4px;
|
||||||
|
border-left-width: 4px;
|
||||||
|
-webkit-border-radius: 9px 4px;
|
||||||
|
-webkit-box-shadow: inset 0 0 0 1px hsl(211, 10%, 53%), inset 0 0 0 4px hsl(209deg 18% 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track-piece {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:horizontal {
|
||||||
|
border-right-width: 0;
|
||||||
|
border-left-width: 0;
|
||||||
|
border-top-width: 4px;
|
||||||
|
border-bottom-width: 4px;
|
||||||
|
-webkit-border-radius: 4px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
-webkit-box-shadow:
|
||||||
|
inset 0 0 0 1px hsl(212, 92%, 43%),
|
||||||
|
inset 0 0 0 4px hsl(212, 92%, 43%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import ReactGA from 'react-ga';
|
||||||
import { hot } from 'react-hot-loader/root';
|
import { hot } from 'react-hot-loader/root';
|
||||||
import { BrowserRouter, Route, Switch } from 'react-router-dom';
|
import { BrowserRouter, Route, Switch } from 'react-router-dom';
|
||||||
import { StoreProvider } from 'easy-peasy';
|
import { StoreProvider } from 'easy-peasy';
|
||||||
|
@ -48,6 +49,11 @@ const App = () => {
|
||||||
store.getActions().settings.setSettings(SiteConfiguration!);
|
store.getActions().settings.setSettings(SiteConfiguration!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ReactGA.initialize(SiteConfiguration!.analytics);
|
||||||
|
ReactGA.pageview(location.pathname);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalStylesheet/>
|
<GlobalStylesheet/>
|
||||||
|
|
|
@ -1,27 +1,40 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
|
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
import { object, string } from 'yup';
|
import { object, string } from 'yup';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
|
import Reaptcha from 'reaptcha';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const ref = useRef<Reaptcha>(null);
|
||||||
|
const [ token, setToken ] = useState('');
|
||||||
|
|
||||||
|
const { clearFlashes, addFlash } = useFlash();
|
||||||
|
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
|
||||||
|
|
||||||
const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
|
const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
|
||||||
setSubmitting(true);
|
|
||||||
clearFlashes();
|
clearFlashes();
|
||||||
requestPasswordResetEmail(email)
|
|
||||||
|
// If there is no token in the state yet, request the token and then abort this submit request
|
||||||
|
// since it will be re-submitted when the recaptcha data is returned by the component.
|
||||||
|
if (recaptchaEnabled && !token) {
|
||||||
|
ref.current!.execute().catch(error => console.error(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPasswordResetEmail(email, token)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
resetForm();
|
resetForm();
|
||||||
addFlash({ type: 'success', title: 'Success', message: response });
|
addFlash({ type: 'success', title: 'Success', message: response });
|
||||||
|
@ -42,7 +55,7 @@ export default () => {
|
||||||
.required('A valid email address must be provided to continue.'),
|
.required('A valid email address must be provided to continue.'),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting, setSubmitting, submitForm }) => (
|
||||||
<LoginFormContainer
|
<LoginFormContainer
|
||||||
title={'Request Password Reset'}
|
title={'Request Password Reset'}
|
||||||
css={tw`w-full flex`}
|
css={tw`w-full flex`}
|
||||||
|
@ -64,6 +77,21 @@ export default () => {
|
||||||
Send Email
|
Send Email
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{recaptchaEnabled &&
|
||||||
|
<Reaptcha
|
||||||
|
ref={ref}
|
||||||
|
size={'invisible'}
|
||||||
|
sitekey={siteKey || '_invalid_key'}
|
||||||
|
onVerify={response => {
|
||||||
|
setToken(response);
|
||||||
|
submitForm();
|
||||||
|
}}
|
||||||
|
onExpire={() => {
|
||||||
|
setSubmitting(false);
|
||||||
|
setToken('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
<div css={tw`mt-6 text-center`}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
type={'button'}
|
type={'button'}
|
||||||
|
|
|
@ -1,41 +1,67 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import login, { LoginData } from '@/api/auth/login';
|
import login from '@/api/auth/login';
|
||||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||||
import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
import { FormikProps, withFormik } from 'formik';
|
|
||||||
import { object, string } from 'yup';
|
import { object, string } from 'yup';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
|
||||||
import { FlashMessage } from '@/state/flashes';
|
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
|
import Reaptcha from 'reaptcha';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
|
||||||
type OwnProps = RouteComponentProps & {
|
interface Values {
|
||||||
clearFlashes: ActionCreator<void>;
|
username: string;
|
||||||
addFlash: ActionCreator<FlashMessage>;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps<LoginData>) => {
|
const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||||
const ref = useRef<ReCAPTCHA | null>(null);
|
const ref = useRef<Reaptcha>(null);
|
||||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState<ApplicationStore, any>(state => state.settings.data!.recaptcha);
|
const [ token, setToken ] = useState('');
|
||||||
|
|
||||||
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
e.preventDefault();
|
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
|
||||||
|
|
||||||
if (ref.current && !values.recaptchaData) {
|
const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
return ref.current.execute();
|
clearFlashes();
|
||||||
|
|
||||||
|
// If there is no token in the state yet, request the token and then abort this submit request
|
||||||
|
// since it will be re-submitted when the recaptcha data is returned by the component.
|
||||||
|
if (recaptchaEnabled && !token) {
|
||||||
|
ref.current!.execute().catch(error => console.error(error));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit(e);
|
login({ ...values, recaptchaData: token })
|
||||||
|
.then(response => {
|
||||||
|
if (response.complete) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.location = response.intended || '/';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
setSubmitting(false);
|
||||||
|
clearAndAddHttpError({ error });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<Formik
|
||||||
{ref.current && ref.current.render()}
|
onSubmit={onSubmit}
|
||||||
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} onSubmit={submit}>
|
initialValues={{ username: '', password: '' }}
|
||||||
|
validationSchema={object().shape({
|
||||||
|
username: string().required('A username or email must be provided.'),
|
||||||
|
password: string().required('Please enter your account password.'),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, setSubmitting, submitForm }) => (
|
||||||
|
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}>
|
||||||
<Field
|
<Field
|
||||||
type={'text'}
|
type={'text'}
|
||||||
label={'Username or Email'}
|
label={'Username or Email'}
|
||||||
|
@ -58,16 +84,18 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{recaptchaEnabled &&
|
{recaptchaEnabled &&
|
||||||
<ReCAPTCHA
|
<Reaptcha
|
||||||
ref={ref}
|
ref={ref}
|
||||||
size={'invisible'}
|
size={'invisible'}
|
||||||
sitekey={siteKey || '_invalid_key'}
|
sitekey={siteKey || '_invalid_key'}
|
||||||
onChange={token => {
|
onVerify={response => {
|
||||||
ref.current && ref.current.reset();
|
setToken(response);
|
||||||
setFieldValue('recaptchaData', token);
|
|
||||||
submitForm();
|
submitForm();
|
||||||
}}
|
}}
|
||||||
onExpired={() => setFieldValue('recaptchaData', null)}
|
onExpire={() => {
|
||||||
|
setSubmitting(false);
|
||||||
|
setToken('');
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<div css={tw`mt-6 text-center`}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
|
@ -79,54 +107,9 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</LoginFormContainer>
|
</LoginFormContainer>
|
||||||
</React.Fragment>
|
)}
|
||||||
|
</Formik>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnhancedForm = withFormik<OwnProps, LoginData>({
|
export default LoginContainer;
|
||||||
displayName: 'LoginContainerForm',
|
|
||||||
|
|
||||||
mapPropsToValues: () => ({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
recaptchaData: null,
|
|
||||||
}),
|
|
||||||
|
|
||||||
validationSchema: () => object().shape({
|
|
||||||
username: string().required('A username or email must be provided.'),
|
|
||||||
password: string().required('Please enter your account password.'),
|
|
||||||
}),
|
|
||||||
|
|
||||||
handleSubmit: (values, { props, setFieldValue, setSubmitting }) => {
|
|
||||||
props.clearFlashes();
|
|
||||||
login(values)
|
|
||||||
.then(response => {
|
|
||||||
if (response.complete) {
|
|
||||||
// @ts-ignore
|
|
||||||
window.location = response.intended || '/';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
props.history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
setSubmitting(false);
|
|
||||||
setFieldValue('recaptchaData', null);
|
|
||||||
props.addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})(LoginContainer);
|
|
||||||
|
|
||||||
export default (props: RouteComponentProps) => {
|
|
||||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedForm
|
|
||||||
{...props}
|
|
||||||
addFlash={addFlash}
|
|
||||||
clearFlashes={clearFlashes}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import ContentBox from '@/components/elements/ContentBox';
|
import ContentBox from '@/components/elements/ContentBox';
|
||||||
import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm';
|
import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm';
|
||||||
import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
|
import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
|
||||||
|
@ -7,7 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
||||||
import deleteApiKey from '@/api/account/deleteApiKey';
|
import deleteApiKey from '@/api/account/deleteApiKey';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
|
@ -21,6 +22,7 @@ export default () => {
|
||||||
const [ keys, setKeys ] = useState<ApiKey[]>([]);
|
const [ keys, setKeys ] = useState<ApiKey[]>([]);
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearFlashes('account');
|
clearFlashes('account');
|
||||||
|
@ -49,6 +51,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
|
<Helmet>
|
||||||
|
<title> {name} | API</title>
|
||||||
|
</Helmet>
|
||||||
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
|
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
|
||||||
<div css={tw`flex`}>
|
<div css={tw`flex`}>
|
||||||
<ContentBox title={'Create API Key'} css={tw`flex-1`}>
|
<ContentBox title={'Create API Key'} css={tw`flex-1`}>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { ApplicationStore } from '@/state';
|
||||||
import ContentBox from '@/components/elements/ContentBox';
|
import ContentBox from '@/components/elements/ContentBox';
|
||||||
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
|
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
|
||||||
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
|
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
|
||||||
|
@ -7,6 +9,7 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { breakpoint } from '@/theme';
|
import { breakpoint } from '@/theme';
|
||||||
import styled from 'styled-components/macro';
|
import styled from 'styled-components/macro';
|
||||||
|
import { useStoreState } from 'easy-peasy';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
${tw`flex flex-wrap my-10`};
|
${tw`flex flex-wrap my-10`};
|
||||||
|
@ -25,8 +28,12 @@ const Container = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
|
<Helmet>
|
||||||
|
<title> {name} | Account Overview</title>
|
||||||
|
</Helmet>
|
||||||
<Container>
|
<Container>
|
||||||
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
|
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
|
||||||
<UpdatePasswordForm/>
|
<UpdatePasswordForm/>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import { Server } from '@/api/server/getServer';
|
import { Server } from '@/api/server/getServer';
|
||||||
|
import { ApplicationStore } from '@/state';
|
||||||
import getServers from '@/api/getServers';
|
import getServers from '@/api/getServers';
|
||||||
import ServerRow from '@/components/dashboard/ServerRow';
|
import ServerRow from '@/components/dashboard/ServerRow';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
|
@ -18,6 +20,7 @@ export default () => {
|
||||||
const [ page, setPage ] = useState(1);
|
const [ page, setPage ] = useState(1);
|
||||||
const { rootAdmin } = useStoreState(state => state.user.data!);
|
const { rootAdmin } = useStoreState(state => state.user.data!);
|
||||||
const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState('show_all_servers', false);
|
const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState('show_all_servers', false);
|
||||||
|
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||||
|
|
||||||
const { data: servers, error } = useSWR<PaginatedResult<Server>>(
|
const { data: servers, error } = useSWR<PaginatedResult<Server>>(
|
||||||
[ '/api/client/servers', showOnlyAdmin, page ],
|
[ '/api/client/servers', showOnlyAdmin, page ],
|
||||||
|
@ -31,6 +34,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock showFlashKey={'dashboard'}>
|
<PageContentBlock showFlashKey={'dashboard'}>
|
||||||
|
<Helmet>
|
||||||
|
<title> {name} | Dashboard</title>
|
||||||
|
</Helmet>
|
||||||
{rootAdmin &&
|
{rootAdmin &&
|
||||||
<div css={tw`mb-2 flex justify-end items-center`}>
|
<div css={tw`mb-2 flex justify-end items-center`}>
|
||||||
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
|
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
|
||||||
|
|
26
resources/scripts/components/server/InstallListener.tsx
Normal file
26
resources/scripts/components/server/InstallListener.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
|
||||||
|
const InstallListener = () => {
|
||||||
|
const server = useServer();
|
||||||
|
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||||
|
const setServer = ServerContext.useStoreActions(actions => actions.server.setServer);
|
||||||
|
|
||||||
|
// Listen for the installation completion event and then fire off a request to fetch the updated
|
||||||
|
// server information. This allows the server to automatically become available to the user if they
|
||||||
|
// just sit on the page.
|
||||||
|
useWebsocketEvent('install completed', () => {
|
||||||
|
getServer(server.uuid).catch(error => console.error(error));
|
||||||
|
});
|
||||||
|
|
||||||
|
// When we see the install started event immediately update the state to indicate such so that the
|
||||||
|
// screens automatically update.
|
||||||
|
useWebsocketEvent('install started', () => {
|
||||||
|
setServer({ ...server, isInstalling: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InstallListener;
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { lazy, useEffect, useState } from 'react';
|
import React, { lazy, useEffect, useState } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
@ -61,6 +62,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock css={tw`flex`}>
|
<PageContentBlock css={tw`flex`}>
|
||||||
|
<Helmet>
|
||||||
|
<title> {server.name} | Console </title>
|
||||||
|
</Helmet>
|
||||||
<div css={tw`w-1/4`}>
|
<div css={tw`w-1/4`}>
|
||||||
<TitledGreyBox title={server.name} icon={faServer}>
|
<TitledGreyBox title={server.name} icon={faServer}>
|
||||||
<p css={tw`text-xs uppercase`}>
|
<p css={tw`text-xs uppercase`}>
|
||||||
|
|
|
@ -8,7 +8,7 @@ const StopOrKillButton = ({ onPress }: { onPress: (action: PowerAction) => void
|
||||||
const status = ServerContext.useStoreState(state => state.status.value);
|
const status = ServerContext.useStoreState(state => state.status.value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setClicked(state => [ 'stopping' ].indexOf(status) < 0 ? false : state);
|
setClicked(status === 'stopping');
|
||||||
}, [ status ]);
|
}, [ status ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
import getServerBackups from '@/api/server/backups/getServerBackups';
|
import getServerBackups from '@/api/server/backups/getServerBackups';
|
||||||
import useServer from '@/plugins/useServer';
|
import useServer from '@/plugins/useServer';
|
||||||
|
@ -13,7 +14,7 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { uuid, featureLimits } = useServer();
|
const { uuid, featureLimits, name: serverName } = useServer();
|
||||||
const { addError, clearFlashes } = useFlash();
|
const { addError, clearFlashes } = useFlash();
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
|
|
||||||
|
@ -37,6 +38,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
|
<Helmet>
|
||||||
|
<title> {serverName} | Backups</title>
|
||||||
|
</Helmet>
|
||||||
<FlashMessageRender byKey={'backups'} css={tw`mb-4`}/>
|
<FlashMessageRender byKey={'backups'} css={tw`mb-4`}/>
|
||||||
{!backups.length ?
|
{!backups.length ?
|
||||||
<p css={tw`text-center text-sm text-neutral-400`}>
|
<p css={tw`text-center text-sm text-neutral-400`}>
|
||||||
|
@ -52,7 +56,7 @@ export default () => {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{featureLimits.backups === 0 &&
|
{featureLimits.backups === 0 &&
|
||||||
<p className="text-center text-sm text-neutral-400">
|
<p css={tw`text-center text-sm text-neutral-400`}>
|
||||||
Backups cannot be created for this server.
|
Backups cannot be created for this server.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`flex justify-end`}>
|
<div css={tw`flex justify-end`}>
|
||||||
<Button type={'submit'}>
|
<Button type={'submit'} disabled={isSubmitting}>
|
||||||
Start backup
|
Start backup
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -94,11 +94,7 @@ export default () => {
|
||||||
ignored: string(),
|
ignored: string(),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent appear visible={visible} onDismissed={() => setVisible(false)}/>
|
||||||
appear
|
|
||||||
visible={visible}
|
|
||||||
onDismissed={() => setVisible(false)}
|
|
||||||
/>
|
|
||||||
</Formik>
|
</Formik>
|
||||||
}
|
}
|
||||||
<Button onClick={() => setVisible(true)}>
|
<Button onClick={() => setVisible(true)}>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import getServerDatabases from '@/api/server/getServerDatabases';
|
import getServerDatabases from '@/api/server/getServerDatabases';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
|
@ -14,7 +15,7 @@ import tw from 'twin.macro';
|
||||||
import Fade from '@/components/elements/Fade';
|
import Fade from '@/components/elements/Fade';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { uuid, featureLimits } = useServer();
|
const { uuid, featureLimits, name: serverName } = useServer();
|
||||||
const { addError, clearFlashes } = useFlash();
|
const { addError, clearFlashes } = useFlash();
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
|
|
||||||
|
@ -36,6 +37,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
|
<Helmet>
|
||||||
|
<title> {serverName} | Databases </title>
|
||||||
|
</Helmet>
|
||||||
<FlashMessageRender byKey={'databases'} css={tw`mb-4`}/>
|
<FlashMessageRender byKey={'databases'} css={tw`mb-4`}/>
|
||||||
{(!databases.length && loading) ?
|
{(!databases.length && loading) ?
|
||||||
<Spinner size={'large'} centered/>
|
<Spinner size={'large'} centered/>
|
||||||
|
|
10
resources/scripts/components/server/events.ts
Normal file
10
resources/scripts/components/server/events.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export enum SocketEvent {
|
||||||
|
DAEMON_MESSAGE = 'daemon message',
|
||||||
|
INSTALL_OUTPUT = 'install output',
|
||||||
|
INSTALL_STARTED = 'install started',
|
||||||
|
INSTALL_COMPLETED = 'install completed',
|
||||||
|
CONSOLE_OUTPUT = 'console output',
|
||||||
|
STATUS = 'status',
|
||||||
|
STATS = 'stats',
|
||||||
|
BACKUP_COMPLETED = 'backup completed',
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { memo, useRef, useState } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import {
|
import {
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
|
@ -29,6 +29,7 @@ import styled from 'styled-components/macro';
|
||||||
import useEventListener from '@/plugins/useEventListener';
|
import useEventListener from '@/plugins/useEventListener';
|
||||||
import compressFiles from '@/api/server/files/compressFiles';
|
import compressFiles from '@/api/server/files/compressFiles';
|
||||||
import decompressFiles from '@/api/server/files/decompressFiles';
|
import decompressFiles from '@/api/server/files/decompressFiles';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
|
||||||
type ModalType = 'rename' | 'move';
|
type ModalType = 'rename' | 'move';
|
||||||
|
|
||||||
|
@ -50,7 +51,7 @@ const Row = ({ icon, title, ...props }: RowProps) => (
|
||||||
</StyledRow>
|
</StyledRow>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ({ file }: { file: FileObject }) => {
|
const FileDropdownMenu = ({ file }: { file: FileObject }) => {
|
||||||
const onClickRef = useRef<DropdownMenu>(null);
|
const onClickRef = useRef<DropdownMenu>(null);
|
||||||
const [ showSpinner, setShowSpinner ] = useState(false);
|
const [ showSpinner, setShowSpinner ] = useState(false);
|
||||||
const [ modal, setModal ] = useState<ModalType | null>(null);
|
const [ modal, setModal ] = useState<ModalType | null>(null);
|
||||||
|
@ -60,7 +61,7 @@ export default ({ file }: { file: FileObject }) => {
|
||||||
const { clearAndAddHttpError, clearFlashes } = useFlash();
|
const { clearAndAddHttpError, clearFlashes } = useFlash();
|
||||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||||
|
|
||||||
useEventListener(`pterodactyl:files:ctx:${file.uuid}`, (e: CustomEvent) => {
|
useEventListener(`pterodactyl:files:ctx:${file.key}`, (e: CustomEvent) => {
|
||||||
if (onClickRef.current) {
|
if (onClickRef.current) {
|
||||||
onClickRef.current.triggerMenu(e.detail);
|
onClickRef.current.triggerMenu(e.detail);
|
||||||
}
|
}
|
||||||
|
@ -71,7 +72,7 @@ export default ({ file }: { file: FileObject }) => {
|
||||||
|
|
||||||
// For UI speed, immediately remove the file from the listing before calling the deletion function.
|
// For UI speed, immediately remove the file from the listing before calling the deletion function.
|
||||||
// If the delete actually fails, we'll fetch the current directory contents again automatically.
|
// If the delete actually fails, we'll fetch the current directory contents again automatically.
|
||||||
mutate(files => files.filter(f => f.uuid !== file.uuid), false);
|
mutate(files => files.filter(f => f.key !== file.key), false);
|
||||||
|
|
||||||
deleteFiles(uuid, directory, [ file.name ]).catch(error => {
|
deleteFiles(uuid, directory, [ file.name ]).catch(error => {
|
||||||
mutate();
|
mutate();
|
||||||
|
@ -166,3 +167,5 @@ export default ({ file }: { file: FileObject }) => {
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default memo(FileDropdownMenu, isEqual);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
|
@ -23,9 +24,10 @@ const sortFiles = (files: FileObject[]): FileObject[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { id } = useServer();
|
const { id, name: serverName } = useServer();
|
||||||
const { hash } = useLocation();
|
const { hash } = useLocation();
|
||||||
const { data: files, error, mutate } = useFileManagerSwr();
|
const { data: files, error, mutate } = useFileManagerSwr();
|
||||||
|
|
||||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||||
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
|
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
|
||||||
|
|
||||||
|
@ -42,6 +44,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock showFlashKey={'files'}>
|
<PageContentBlock showFlashKey={'files'}>
|
||||||
|
<Helmet>
|
||||||
|
<title> {serverName} | File Manager </title>
|
||||||
|
</Helmet>
|
||||||
<FileManagerBreadcrumbs/>
|
<FileManagerBreadcrumbs/>
|
||||||
{
|
{
|
||||||
!files ?
|
!files ?
|
||||||
|
@ -65,7 +70,7 @@ export default () => {
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
sortFiles(files.slice(0, 250)).map(file => (
|
sortFiles(files.slice(0, 250)).map(file => (
|
||||||
<FileObjectRow key={file.uuid} file={file}/>
|
<FileObjectRow key={file.key} file={file}/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
<MassActionsBar/>
|
<MassActionsBar/>
|
||||||
|
|
|
@ -39,7 +39,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
|
||||||
key={file.name}
|
key={file.name}
|
||||||
onContextMenu={e => {
|
onContextMenu={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.uuid}`, { detail: e.clientX }));
|
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.key}`, { detail: e.clientX }));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectFileCheckbox name={file.name}/>
|
<SelectFileCheckbox name={file.name}/>
|
||||||
|
|
|
@ -6,14 +6,12 @@ import Field from '@/components/elements/Field';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { object, string } from 'yup';
|
import { object, string } from 'yup';
|
||||||
import createDirectory from '@/api/server/files/createDirectory';
|
import createDirectory from '@/api/server/files/createDirectory';
|
||||||
import v4 from 'uuid/v4';
|
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
import { mutate } from 'swr';
|
|
||||||
import useServer from '@/plugins/useServer';
|
import useServer from '@/plugins/useServer';
|
||||||
import { FileObject } from '@/api/server/files/loadDirectory';
|
import { FileObject } from '@/api/server/files/loadDirectory';
|
||||||
import { useLocation } from 'react-router';
|
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
directoryName: string;
|
directoryName: string;
|
||||||
|
@ -24,7 +22,7 @@ const schema = object().shape({
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateDirectoryData = (name: string): FileObject => ({
|
const generateDirectoryData = (name: string): FileObject => ({
|
||||||
uuid: v4(),
|
key: `dir_${name}`,
|
||||||
name: name,
|
name: name,
|
||||||
mode: '0644',
|
mode: '0644',
|
||||||
size: 0,
|
size: 0,
|
||||||
|
@ -39,20 +37,16 @@ const generateDirectoryData = (name: string): FileObject => ({
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { uuid } = useServer();
|
const { uuid } = useServer();
|
||||||
const { hash } = useLocation();
|
|
||||||
const { clearAndAddHttpError } = useFlash();
|
const { clearAndAddHttpError } = useFlash();
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
|
|
||||||
|
const { mutate } = useFileManagerSwr();
|
||||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||||
|
|
||||||
const submit = ({ directoryName }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const submit = ({ directoryName }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
createDirectory(uuid, directory, directoryName)
|
createDirectory(uuid, directory, directoryName)
|
||||||
.then(() => {
|
.then(() => mutate(data => [ ...data, generateDirectoryData(directoryName) ], false))
|
||||||
mutate(
|
.then(() => setVisible(false))
|
||||||
`${uuid}:files:${hash}`,
|
|
||||||
(data: FileObject[]) => [ ...data, generateDirectoryData(directoryName) ],
|
|
||||||
);
|
|
||||||
setVisible(false);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
@ -79,6 +73,7 @@ export default () => {
|
||||||
>
|
>
|
||||||
<Form css={tw`m-0`}>
|
<Form css={tw`m-0`}>
|
||||||
<Field
|
<Field
|
||||||
|
autoFocus
|
||||||
id={'directoryName'}
|
id={'directoryName'}
|
||||||
name={'directoryName'}
|
name={'directoryName'}
|
||||||
label={'Directory Name'}
|
label={'Directory Name'}
|
||||||
|
|
|
@ -15,9 +15,9 @@ interface FormikValues {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean };
|
type OwnProps = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean };
|
||||||
|
|
||||||
export default ({ files, useMoveTerminology, ...props }: Props) => {
|
const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => {
|
||||||
const { uuid } = useServer();
|
const { uuid } = useServer();
|
||||||
const { mutate } = useFileManagerSwr();
|
const { mutate } = useFileManagerSwr();
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
|
@ -96,3 +96,5 @@ export default ({ files, useMoveTerminology, ...props }: Props) => {
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default RenameFileModal;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
@ -23,7 +24,7 @@ const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm
|
||||||
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
||||||
|
|
||||||
const NetworkContainer = () => {
|
const NetworkContainer = () => {
|
||||||
const { uuid, allocations } = useServer();
|
const { uuid, allocations, name: serverName } = useServer();
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
const [ loading, setLoading ] = useState<false | number>(false);
|
const [ loading, setLoading ] = useState<false | number>(false);
|
||||||
const { data, error, mutate } = useSWR<Allocation[]>(uuid, key => getServerAllocations(key), { initialData: allocations });
|
const { data, error, mutate } = useSWR<Allocation[]>(uuid, key => getServerAllocations(key), { initialData: allocations });
|
||||||
|
@ -61,6 +62,9 @@ const NetworkContainer = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock showFlashKey={'server:network'}>
|
<PageContentBlock showFlashKey={'server:network'}>
|
||||||
|
<Helmet>
|
||||||
|
<title> {serverName} | Network </title>
|
||||||
|
</Helmet>
|
||||||
{!data ?
|
{!data ?
|
||||||
<Spinner size={'large'} centered/>
|
<Spinner size={'large'} centered/>
|
||||||
:
|
:
|
||||||
|
|
|
@ -65,7 +65,7 @@ const EditScheduleModal = ({ schedule, ...props }: Omit<Props, 'onScheduleUpdate
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6 text-right`}>
|
<div css={tw`mt-6 text-right`}>
|
||||||
<Button type={'submit'}>
|
<Button type={'submit'} disabled={isSubmitting}>
|
||||||
{schedule ? 'Save changes' : 'Create schedule'}
|
{schedule ? 'Save changes' : 'Create schedule'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import getServerSchedules from '@/api/server/schedules/getServerSchedules';
|
import getServerSchedules from '@/api/server/schedules/getServerSchedules';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
|
@ -16,7 +17,7 @@ import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
export default ({ match, history }: RouteComponentProps) => {
|
export default ({ match, history }: RouteComponentProps) => {
|
||||||
const { uuid } = useServer();
|
const { uuid, name: serverName } = useServer();
|
||||||
const { clearFlashes, addError } = useFlash();
|
const { clearFlashes, addError } = useFlash();
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
|
@ -37,6 +38,9 @@ export default ({ match, history }: RouteComponentProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
|
<Helmet>
|
||||||
|
<title> {serverName} | Schedules </title>
|
||||||
|
</Helmet>
|
||||||
<FlashMessageRender byKey={'schedules'} css={tw`mb-4`}/>
|
<FlashMessageRender byKey={'schedules'} css={tw`mb-4`}/>
|
||||||
{(!schedules.length && loading) ?
|
{(!schedules.length && loading) ?
|
||||||
<Spinner size={'large'} centered/>
|
<Spinner size={'large'} centered/>
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default ({ schedule }: { schedule: Schedule }) => (
|
||||||
<p>{schedule.name}</p>
|
<p>{schedule.name}</p>
|
||||||
<p css={tw`text-xs text-neutral-400`}>
|
<p css={tw`text-xs text-neutral-400`}>
|
||||||
Last run
|
Last run
|
||||||
at: {schedule.lastRunAt ? format(schedule.lastRunAt, 'MMM Do [at] h:mma') : 'never'}
|
at: {schedule.lastRunAt ? format(schedule.lastRunAt, 'MMM do \'at\' h:mma') : 'never'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`flex items-center mx-8`}>
|
<div css={tw`flex items-center mx-8`}>
|
||||||
|
|
|
@ -32,11 +32,16 @@ interface Values {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
||||||
const { values: { action }, setFieldValue, setFieldTouched } = useFormikContext<Values>();
|
const { values: { action }, initialValues, setFieldValue, setFieldTouched, isSubmitting } = useFormikContext<Values>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (action !== initialValues.action) {
|
||||||
setFieldValue('payload', action === 'power' ? 'start' : '');
|
setFieldValue('payload', action === 'power' ? 'start' : '');
|
||||||
setFieldTouched('payload', false);
|
setFieldTouched('payload', false);
|
||||||
|
} else {
|
||||||
|
setFieldValue('payload', initialValues.payload);
|
||||||
|
setFieldTouched('payload', false);
|
||||||
|
}
|
||||||
}, [ action ]);
|
}, [ action ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -94,7 +99,7 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`flex justify-end mt-6`}>
|
<div css={tw`flex justify-end mt-6`}>
|
||||||
<Button type={'submit'}>
|
<Button type={'submit'} disabled={isSubmitting}>
|
||||||
{isEditingTask ? 'Save Changes' : 'Create Task'}
|
{isEditingTask ? 'Save Changes' : 'Create Task'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||||
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
import ConfirmationModal from '@/components/elements/ConfirmationModal';
|
||||||
|
@ -37,6 +37,10 @@ export default () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearFlashes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TitledGreyBox title={'Reinstall Server'} css={tw`relative`}>
|
<TitledGreyBox title={'Reinstall Server'} css={tw`relative`}>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { useStoreState } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
|
@ -20,6 +21,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
|
<Helmet>
|
||||||
|
<title> {server.name} | Settings </title>
|
||||||
|
</Helmet>
|
||||||
<FlashMessageRender byKey={'settings'} css={tw`mb-4`}/>
|
<FlashMessageRender byKey={'settings'} css={tw`mb-4`}/>
|
||||||
<div css={tw`md:flex`}>
|
<div css={tw`md:flex`}>
|
||||||
<div css={tw`w-full md:flex-1 md:mr-10`}>
|
<div css={tw`w-full md:flex-1 md:mr-10`}>
|
||||||
|
|
|
@ -51,17 +51,18 @@ export default ({ subuser }: Props) => {
|
||||||
</p>
|
</p>
|
||||||
<p css={tw`text-2xs text-neutral-500 uppercase`}>Permissions</p>
|
<p css={tw`text-2xs text-neutral-500 uppercase`}>Permissions</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Can action={'user.update'}>
|
||||||
|
{subuser.uuid !== uuid &&
|
||||||
<button
|
<button
|
||||||
type={'button'}
|
type={'button'}
|
||||||
aria-label={'Edit subuser'}
|
aria-label={'Edit subuser'}
|
||||||
css={[
|
css={tw`block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4`}
|
||||||
tw`block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mx-4`,
|
|
||||||
subuser.uuid === uuid ? tw`hidden` : undefined,
|
|
||||||
]}
|
|
||||||
onClick={() => setVisible(true)}
|
onClick={() => setVisible(true)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencilAlt}/>
|
<FontAwesomeIcon icon={faPencilAlt}/>
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
|
</Can>
|
||||||
<Can action={'user.delete'}>
|
<Can action={'user.delete'}>
|
||||||
<RemoveSubuserButton subuser={subuser}/>
|
<RemoveSubuserButton subuser={subuser}/>
|
||||||
</Can>
|
</Can>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
|
@ -17,6 +18,7 @@ export default () => {
|
||||||
|
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||||
const subusers = ServerContext.useStoreState(state => state.subusers.data);
|
const subusers = ServerContext.useStoreState(state => state.subusers.data);
|
||||||
|
const servername = ServerContext.useStoreState(state => state.server.data!.name);
|
||||||
const setSubusers = ServerContext.useStoreActions(actions => actions.subusers.setSubusers);
|
const setSubusers = ServerContext.useStoreActions(actions => actions.subusers.setSubusers);
|
||||||
|
|
||||||
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
|
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
|
||||||
|
@ -49,6 +51,9 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
|
<Helmet>
|
||||||
|
<title> {servername} | Subusers </title>
|
||||||
|
</Helmet>
|
||||||
<FlashMessageRender byKey={'users'} css={tw`mb-4`}/>
|
<FlashMessageRender byKey={'users'} css={tw`mb-4`}/>
|
||||||
{!subusers.length ?
|
{!subusers.length ?
|
||||||
<p css={tw`text-center text-sm text-neutral-400`}>
|
<p css={tw`text-center text-sm text-neutral-400`}>
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
import Sockette from 'sockette';
|
import Sockette from 'sockette';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
export const SOCKET_EVENTS = [
|
|
||||||
'SOCKET_OPEN',
|
|
||||||
'SOCKET_RECONNECT',
|
|
||||||
'SOCKET_CLOSE',
|
|
||||||
'SOCKET_ERROR',
|
|
||||||
];
|
|
||||||
|
|
||||||
export class Websocket extends EventEmitter {
|
export class Websocket extends EventEmitter {
|
||||||
// Timer instance for this socket.
|
// Timer instance for this socket.
|
||||||
private timer: any = null;
|
private timer: any = null;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { DependencyList } from 'react';
|
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { Server } from '@/api/server/getServer';
|
import { Server } from '@/api/server/getServer';
|
||||||
|
|
||||||
const useServer = (dependencies?: DependencyList): Server => {
|
const useServer = (dependencies?: any[] | undefined): Server => {
|
||||||
return ServerContext.useStoreState(state => state.server.data!, [ dependencies ]);
|
return ServerContext.useStoreState(state => state.server.data!, dependencies);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useServer;
|
export default useServer;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import ReactGA from 'react-ga';
|
||||||
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||||
import LoginContainer from '@/components/auth/LoginContainer';
|
import LoginContainer from '@/components/auth/LoginContainer';
|
||||||
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
|
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
|
||||||
|
@ -6,17 +7,23 @@ import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
|
||||||
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
|
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
|
||||||
import NotFound from '@/components/screens/NotFound';
|
import NotFound from '@/components/screens/NotFound';
|
||||||
|
|
||||||
export default ({ location, history, match }: RouteComponentProps) => (
|
export default ({ location, history, match }: RouteComponentProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
ReactGA.pageview(location.pathname);
|
||||||
|
}, [ location.pathname ]);
|
||||||
|
|
||||||
|
return (
|
||||||
<div className={'pt-8 xl:pt-32'}>
|
<div className={'pt-8 xl:pt-32'}>
|
||||||
<Switch location={location}>
|
<Switch location={location}>
|
||||||
<Route path={`${match.path}/login`} component={LoginContainer} exact/>
|
<Route path={`${match.path}/login`} component={LoginContainer} exact/>
|
||||||
<Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
|
<Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
|
||||||
<Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
|
<Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
|
||||||
<Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
|
<Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
|
||||||
<Route path={`${match.path}/checkpoint`}/>
|
<Route path={`${match.path}/checkpoint`} />
|
||||||
<Route path={'*'}>
|
<Route path={'*'}>
|
||||||
<NotFound onBack={() => history.push('/auth/login')}/>
|
<NotFound onBack={() => history.push('/auth/login')} />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import ReactGA from 'react-ga';
|
||||||
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||||
import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer';
|
import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer';
|
||||||
import NavigationBar from '@/components/NavigationBar';
|
import NavigationBar from '@/components/NavigationBar';
|
||||||
|
@ -8,9 +9,14 @@ import NotFound from '@/components/screens/NotFound';
|
||||||
import TransitionRouter from '@/TransitionRouter';
|
import TransitionRouter from '@/TransitionRouter';
|
||||||
import SubNavigation from '@/components/elements/SubNavigation';
|
import SubNavigation from '@/components/elements/SubNavigation';
|
||||||
|
|
||||||
export default ({ location }: RouteComponentProps) => (
|
export default ({ location }: RouteComponentProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
ReactGA.pageview(location.pathname);
|
||||||
|
}, [ location.pathname ]);
|
||||||
|
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<NavigationBar/>
|
<NavigationBar />
|
||||||
{location.pathname.startsWith('/account') &&
|
{location.pathname.startsWith('/account') &&
|
||||||
<SubNavigation>
|
<SubNavigation>
|
||||||
<div>
|
<div>
|
||||||
|
@ -21,11 +27,12 @@ export default ({ location }: RouteComponentProps) => (
|
||||||
}
|
}
|
||||||
<TransitionRouter>
|
<TransitionRouter>
|
||||||
<Switch location={location}>
|
<Switch location={location}>
|
||||||
<Route path={'/'} component={DashboardContainer} exact/>
|
<Route path={'/'} component={DashboardContainer} exact />
|
||||||
<Route path={'/account'} component={AccountOverviewContainer} exact/>
|
<Route path={'/account'} component={AccountOverviewContainer} exact/>
|
||||||
<Route path={'/account/api'} component={AccountApiContainer} exact/>
|
<Route path={'/account/api'} component={AccountApiContainer} exact/>
|
||||||
<Route path={'*'} component={NotFound}/>
|
<Route path={'*'} component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</TransitionRouter>
|
</TransitionRouter>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import ReactGA from 'react-ga';
|
||||||
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||||
import NavigationBar from '@/components/NavigationBar';
|
import NavigationBar from '@/components/NavigationBar';
|
||||||
import ServerConsole from '@/components/server/ServerConsole';
|
import ServerConsole from '@/components/server/ServerConsole';
|
||||||
|
@ -25,6 +26,7 @@ import useServer from '@/plugins/useServer';
|
||||||
import ScreenBlock from '@/components/screens/ScreenBlock';
|
import ScreenBlock from '@/components/screens/ScreenBlock';
|
||||||
import SubNavigation from '@/components/elements/SubNavigation';
|
import SubNavigation from '@/components/elements/SubNavigation';
|
||||||
import NetworkContainer from '@/components/server/network/NetworkContainer';
|
import NetworkContainer from '@/components/server/network/NetworkContainer';
|
||||||
|
import InstallListener from '@/components/server/InstallListener';
|
||||||
|
|
||||||
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
||||||
const { rootAdmin } = useStoreState(state => state.user.data!);
|
const { rootAdmin } = useStoreState(state => state.user.data!);
|
||||||
|
@ -60,6 +62,10 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
};
|
};
|
||||||
}, [ match.params.id ]);
|
}, [ match.params.id ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ReactGA.pageview(location.pathname);
|
||||||
|
}, [ location.pathname ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={'server-router'}>
|
<React.Fragment key={'server-router'}>
|
||||||
<NavigationBar/>
|
<NavigationBar/>
|
||||||
|
@ -98,6 +104,8 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
</div>
|
</div>
|
||||||
</SubNavigation>
|
</SubNavigation>
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
|
<InstallListener/>
|
||||||
|
<WebsocketHandler/>
|
||||||
{(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ?
|
{(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ?
|
||||||
<ScreenBlock
|
<ScreenBlock
|
||||||
title={'Your server is installing.'}
|
title={'Your server is installing.'}
|
||||||
|
@ -106,7 +114,6 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<>
|
<>
|
||||||
<WebsocketHandler/>
|
|
||||||
<TransitionRouter>
|
<TransitionRouter>
|
||||||
<Switch location={location}>
|
<Switch location={location}>
|
||||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
||||||
|
|
|
@ -6,7 +6,7 @@ export interface FlashStore {
|
||||||
items: FlashMessage[];
|
items: FlashMessage[];
|
||||||
addFlash: Action<FlashStore, FlashMessage>;
|
addFlash: Action<FlashStore, FlashMessage>;
|
||||||
addError: Action<FlashStore, { message: string; key?: string }>;
|
addError: Action<FlashStore, { message: string; key?: string }>;
|
||||||
clearAndAddHttpError: Action<FlashStore, { error: any, key: string }>;
|
clearAndAddHttpError: Action<FlashStore, { error: any, key?: string }>;
|
||||||
clearFlashes: Action<FlashStore, string | void>;
|
clearFlashes: Action<FlashStore, string | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ export interface SiteSettings {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
siteKey: string;
|
siteKey: string;
|
||||||
};
|
};
|
||||||
|
analytics: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsStore {
|
export interface SettingsStore {
|
||||||
|
|
|
@ -31,6 +31,13 @@
|
||||||
<p class="text-muted"><small>This is the name that is used throughout the panel and in emails sent to clients.</small></p>
|
<p class="text-muted"><small>This is the name that is used throughout the panel and in emails sent to clients.</small></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
<label class="control-label">Google Analytics</label>
|
||||||
|
<div>
|
||||||
|
<input type="text" class="form-control" name="app:analytics" value="{{ old('app:analytics', config('app.analytics')) }}" />
|
||||||
|
<p class="text-muted"><small>This is your Google Analytics Tracking ID, Ex. UA-123723645-2</small></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group col-md-4">
|
<div class="form-group col-md-4">
|
||||||
<label class="control-label">Require 2-Factor Authentication</label>
|
<label class="control-label">Require 2-Factor Authentication</label>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -26,7 +26,7 @@ Route::group(['middleware' => 'guest'], function () {
|
||||||
// Password reset routes. This endpoint is hit after going through
|
// Password reset routes. This endpoint is hit after going through
|
||||||
// the forgot password routes to acquire a token (or after an account
|
// the forgot password routes to acquire a token (or after an account
|
||||||
// is created).
|
// is created).
|
||||||
Route::post('/password/reset', 'ResetPasswordController')->name('auth.reset-password')->middleware('recaptcha');
|
Route::post('/password/reset', 'ResetPasswordController')->name('auth.reset-password');
|
||||||
|
|
||||||
// Catch any other combinations of routes and pass them off to the Vuejs component.
|
// Catch any other combinations of routes and pass them off to the Vuejs component.
|
||||||
Route::fallback('LoginController@index');
|
Route::fallback('LoginController@index');
|
||||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -1013,9 +1013,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-google-recaptcha@^1.1.1":
|
"@types/react-helmet@^6.0.0":
|
||||||
version "1.1.1"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-1.1.1.tgz#7dd2a4dd15d38d8059a2753cd4a7e3485c9bb3ea"
|
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.0.0.tgz#5b74e44a12662ffb12d1c97ee702cf4e220958cf"
|
||||||
|
integrity sha512-NBMPAxgjpaMooXa51cU1BTgrX6T+hQbMiLm77JhBbfOzPQea3RB5rNpPOD5xGWHIVpGXHd59cltEzIq0qglGcQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
@ -5564,11 +5565,16 @@ react-fast-compare@^2.0.1:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
|
|
||||||
react-fast-compare@^3.2.0:
|
react-fast-compare@^3.1.1, react-fast-compare@^3.2.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||||
|
|
||||||
|
react-ga@^3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.1.2.tgz#e13f211c51a2e5c401ea69cf094b9501fe3c51ce"
|
||||||
|
integrity sha512-OJrMqaHEHbodm+XsnjA6ISBEHTwvpFrxco65mctzl/v3CASMSLSyUkFqz9yYrPDKGBUfNQzKCjuMJwctjlWBbw==
|
||||||
|
|
||||||
react-google-recaptcha@^2.0.1:
|
react-google-recaptcha@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8"
|
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8"
|
||||||
|
@ -5576,6 +5582,16 @@ react-google-recaptcha@^2.0.1:
|
||||||
prop-types "^15.5.0"
|
prop-types "^15.5.0"
|
||||||
react-async-script "^1.1.1"
|
react-async-script "^1.1.1"
|
||||||
|
|
||||||
|
react-helmet@^6.1.0:
|
||||||
|
version "6.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
|
||||||
|
integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==
|
||||||
|
dependencies:
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-fast-compare "^3.1.1"
|
||||||
|
react-side-effect "^2.1.0"
|
||||||
|
|
||||||
react-hot-loader@^4.12.21:
|
react-hot-loader@^4.12.21:
|
||||||
version "4.12.21"
|
version "4.12.21"
|
||||||
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975"
|
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975"
|
||||||
|
@ -5643,6 +5659,11 @@ react-router@5.1.2:
|
||||||
tiny-invariant "^1.0.2"
|
tiny-invariant "^1.0.2"
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
|
|
||||||
|
react-side-effect@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.0.tgz#1ce4a8b4445168c487ed24dab886421f74d380d3"
|
||||||
|
integrity sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg==
|
||||||
|
|
||||||
react-transition-group@^4.4.1:
|
react-transition-group@^4.4.1:
|
||||||
version "4.4.1"
|
version "4.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
|
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
|
||||||
|
@ -5714,6 +5735,11 @@ readdirp@~3.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
picomatch "^2.2.1"
|
picomatch "^2.2.1"
|
||||||
|
|
||||||
|
reaptcha@^1.7.2:
|
||||||
|
version "1.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/reaptcha/-/reaptcha-1.7.2.tgz#d829f54270c241f46501e92a5a7badeb1fcf372d"
|
||||||
|
integrity sha512-/RXiPeMd+fPUGByv+kAaQlCXCsSflZ9bKX5Fcwv9IYGS1oyT2nntL/8zn9IaiUFHL66T1jBtOABcb92g2+3w8w==
|
||||||
|
|
||||||
reduce-css-calc@^2.1.6:
|
reduce-css-calc@^2.1.6:
|
||||||
version "2.1.7"
|
version "2.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"
|
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2"
|
||||||
|
|
Loading…
Reference in a new issue