From 826258787bad944824e4e42e921c2953cef62dc8 Mon Sep 17 00:00:00 2001 From: Daniel Blittschau Date: Sat, 16 May 2020 16:46:07 -0500 Subject: [PATCH 01/27] Fix outdated AdminLTE link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4dd56ba1a..53c62f2b1 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ In addition to our standard nest of supported games, our community is constantly ## Credits 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), [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), From 6df54b7149de43b74dd8b5bba0c4eb48b7eab65c Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Tue, 14 Jul 2020 00:52:35 +0200 Subject: [PATCH 02/27] Remove unused import importing SpinnerOverlay is redundant since it is not used --- resources/scripts/components/dashboard/ServerRow.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/scripts/components/dashboard/ServerRow.tsx b/resources/scripts/components/dashboard/ServerRow.tsx index 39d2278f5..ee756e504 100644 --- a/resources/scripts/components/dashboard/ServerRow.tsx +++ b/resources/scripts/components/dashboard/ServerRow.tsx @@ -3,7 +3,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faServer, faEthernet, faMicrochip, faMemory, faHdd } from '@fortawesome/free-solid-svg-icons'; import { Link } from 'react-router-dom'; import { Server } from '@/api/server/getServer'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage'; import { bytesToHuman, megabytesToHuman } from '@/helpers'; import tw from 'twin.macro'; From 1fe254efc6cbaa8713ae1bdf7ca495c7eb5db9ef Mon Sep 17 00:00:00 2001 From: Charles Morgan Date: Wed, 22 Jul 2020 01:54:49 -0400 Subject: [PATCH 03/27] Re-add scroll bar style, fix missed tw conversion Fixed backup message still using old method of "className" changed to use css={ts} readded scrollbar styling from PR#2118 --- .../scripts/assets/css/GlobalStylesheet.ts | 45 +++++++++++++++++-- .../server/backups/BackupContainer.tsx | 2 +- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/resources/scripts/assets/css/GlobalStylesheet.ts b/resources/scripts/assets/css/GlobalStylesheet.ts index 5cc44cea6..a38dff74e 100644 --- a/resources/scripts/assets/css/GlobalStylesheet.ts +++ b/resources/scripts/assets/css/GlobalStylesheet.ts @@ -6,19 +6,19 @@ export default createGlobalStyle` ${tw`font-sans bg-neutral-800 text-neutral-200`}; letter-spacing: 0.015em; } - + h1, h2, h3, h4, h5, h6 { ${tw`font-medium tracking-normal font-header`}; } - + p { ${tw`text-neutral-200 leading-snug font-sans`}; } - + form { ${tw`m-0`}; } - + textarea, select, input, button, button:focus, button:focus-visible { ${tw`outline-none`}; } @@ -32,4 +32,41 @@ export default createGlobalStyle` input[type=number] { -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; + } `; diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx index 669f04e84..feb4e5f2e 100644 --- a/resources/scripts/components/server/backups/BackupContainer.tsx +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -52,7 +52,7 @@ export default () => { } {featureLimits.backups === 0 && -

+

Backups cannot be created for this server.

} From cb4f8efbe673bca52d926b3a9e379a6cda9fabf5 Mon Sep 17 00:00:00 2001 From: Charles Morgan Date: Sun, 26 Jul 2020 21:05:54 -0400 Subject: [PATCH 04/27] Add Google Analytics Added Google Analytics to latest dev branch --- .../Settings/BaseSettingsFormRequest.php | 2 + app/Http/ViewComposers/AssetComposer.php | 1 + app/Providers/SettingsServiceProvider.php | 1 + package.json | 1 + resources/scripts/components/App.tsx | 8 ++- .../scripts/routers/AuthenticationRouter.tsx | 37 ++++++++------ resources/scripts/routers/DashboardRouter.tsx | 51 +++++++++++-------- resources/scripts/routers/ServerRouter.tsx | 5 ++ resources/scripts/state/settings.ts | 1 + .../views/admin/settings/index.blade.php | 7 +++ yarn.lock | 5 ++ 11 files changed, 81 insertions(+), 38 deletions(-) diff --git a/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php index 0b02561dd..777761b67 100644 --- a/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php +++ b/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php @@ -19,6 +19,7 @@ class BaseSettingsFormRequest extends AdminFormRequest 'app:name' => 'required|string|max:255', 'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2', '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', 'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication', 'app:locale' => 'Default Language', + 'app:analytics' => 'Google Analytics', ]; } } diff --git a/app/Http/ViewComposers/AssetComposer.php b/app/Http/ViewComposers/AssetComposer.php index 7e8f82dbc..6da825ad4 100644 --- a/app/Http/ViewComposers/AssetComposer.php +++ b/app/Http/ViewComposers/AssetComposer.php @@ -37,6 +37,7 @@ class AssetComposer 'enabled' => config('recaptcha.enabled', false), 'siteKey' => config('recaptcha.website_key') ?? '', ], + 'analytics' => config('app.analytics') ?? '', ]); } } diff --git a/app/Providers/SettingsServiceProvider.php b/app/Providers/SettingsServiceProvider.php index 8a1d4db21..abd88c04b 100644 --- a/app/Providers/SettingsServiceProvider.php +++ b/app/Providers/SettingsServiceProvider.php @@ -21,6 +21,7 @@ class SettingsServiceProvider extends ServiceProvider protected $keys = [ 'app:name', 'app:locale', + 'app:analytics', 'recaptcha:enabled', 'recaptcha:secret_key', 'recaptcha:website_key', diff --git a/package.json b/package.json index 99bcf0d37..3a81f98fa 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "path": "^0.12.7", "query-string": "^6.7.0", "react": "^16.13.1", + "react-ga": "^3.1.2", "react-dom": "npm:@hot-loader/react-dom", "react-fast-compare": "^3.2.0", "react-google-recaptcha": "^2.0.1", diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx index dac7fd102..350387fac 100644 --- a/resources/scripts/components/App.tsx +++ b/resources/scripts/components/App.tsx @@ -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 { BrowserRouter, Route, Switch } from 'react-router-dom'; import { StoreProvider } from 'easy-peasy'; @@ -48,6 +49,11 @@ const App = () => { store.getActions().settings.setSettings(SiteConfiguration!); } + useEffect(() => { + ReactGA.initialize(SiteConfiguration!.analytics); + ReactGA.pageview(location.pathname); + }, []); + return ( <> diff --git a/resources/scripts/routers/AuthenticationRouter.tsx b/resources/scripts/routers/AuthenticationRouter.tsx index a7c687eef..57d1422ca 100644 --- a/resources/scripts/routers/AuthenticationRouter.tsx +++ b/resources/scripts/routers/AuthenticationRouter.tsx @@ -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 LoginContainer from '@/components/auth/LoginContainer'; import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer'; @@ -6,17 +7,23 @@ import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer'; import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer'; import NotFound from '@/components/screens/NotFound'; -export default ({ location, history, match }: RouteComponentProps) => ( -
- - - - - - - - history.push('/auth/login')}/> - - -
-); +export default ({ location, history, match }: RouteComponentProps) => { + useEffect(() => { + ReactGA.pageview(location.pathname); + }, [ location.pathname ]); + + return ( +
+ + + + + + + + history.push('/auth/login')} /> + + +
+ ); +}; diff --git a/resources/scripts/routers/DashboardRouter.tsx b/resources/scripts/routers/DashboardRouter.tsx index 79ebbe4a1..7a895a7e4 100644 --- a/resources/scripts/routers/DashboardRouter.tsx +++ b/resources/scripts/routers/DashboardRouter.tsx @@ -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 AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer'; import NavigationBar from '@/components/NavigationBar'; @@ -8,24 +9,30 @@ import NotFound from '@/components/screens/NotFound'; import TransitionRouter from '@/TransitionRouter'; import SubNavigation from '@/components/elements/SubNavigation'; -export default ({ location }: RouteComponentProps) => ( - <> - - {location.pathname.startsWith('/account') && - -
- Settings - API Credentials -
-
- } - - - - - - - - - -); +export default ({ location }: RouteComponentProps) => { + useEffect(() => { + ReactGA.pageview(location.pathname); + }, [ location.pathname ]); + + return ( + <> + + {location.pathname.startsWith('/account') && + +
+ Settings + API Credentials +
+
+ } + + + + + + + + + + ); +}; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 9df270eaa..2e9ee9ed3 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import ReactGA from 'react-ga'; import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import NavigationBar from '@/components/NavigationBar'; import ServerConsole from '@/components/server/ServerConsole'; @@ -60,6 +61,10 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) }; }, [ match.params.id ]); + useEffect(() => { + ReactGA.pageview(location.pathname); + }, [ location.pathname ]); + return ( diff --git a/resources/scripts/state/settings.ts b/resources/scripts/state/settings.ts index 20dbbdc6e..3eb782d91 100644 --- a/resources/scripts/state/settings.ts +++ b/resources/scripts/state/settings.ts @@ -7,6 +7,7 @@ export interface SiteSettings { enabled: boolean; siteKey: string; }; + analytics: string; } export interface SettingsStore { diff --git a/resources/views/admin/settings/index.blade.php b/resources/views/admin/settings/index.blade.php index 489646dc9..5ccec0dfa 100644 --- a/resources/views/admin/settings/index.blade.php +++ b/resources/views/admin/settings/index.blade.php @@ -31,6 +31,13 @@

This is the name that is used throughout the panel and in emails sent to clients.

+
+ +
+ +

This is your Google Analytics Tracking ID, Ex. UA-123723645-2

+
+
diff --git a/yarn.lock b/yarn.lock index 62c1da6fb..f20fef049 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5569,6 +5569,11 @@ react-fast-compare@^3.2.0: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" 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: version "2.0.1" resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8" From 6d79ad23a5f50c4853f8e305bb123295177b0ed5 Mon Sep 17 00:00:00 2001 From: Charles Morgan Date: Sun, 26 Jul 2020 23:32:24 -0400 Subject: [PATCH 05/27] Attempt 2? 80% sure this isn't how to use react-helmet.... but it works.... --- package.json | 2 ++ .../scripts/components/server/ServerConsole.tsx | 4 ++++ .../server/backups/BackupContainer.tsx | 5 +++++ .../server/databases/DatabasesContainer.tsx | 5 +++++ .../server/files/FileManagerContainer.tsx | 6 ++++++ .../server/network/NetworkContainer.tsx | 7 +++++++ .../server/schedules/ScheduleContainer.tsx | 5 +++++ .../server/settings/SettingsContainer.tsx | 4 ++++ .../components/server/users/UsersContainer.tsx | 5 +++++ yarn.lock | 17 ++++++++++++++++- 10 files changed, 59 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 99bcf0d37..52d866a06 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react-dom": "npm:@hot-loader/react-dom", "react-fast-compare": "^3.2.0", "react-google-recaptcha": "^2.0.1", + "react-helmet": "^6.1.0", "react-hot-loader": "^4.12.21", "react-i18next": "^11.2.1", "react-redux": "^7.1.0", @@ -61,6 +62,7 @@ "@types/query-string": "^6.3.0", "@types/react": "^16.9.41", "@types/react-dom": "^16.9.8", + "@types/react-helmet": "^6.0.0", "@types/react-redux": "^7.1.1", "@types/react-router": "^5.1.3", "@types/react-router-dom": "^5.1.3", diff --git a/resources/scripts/components/server/ServerConsole.tsx b/resources/scripts/components/server/ServerConsole.tsx index e90c86035..74ba4d750 100644 --- a/resources/scripts/components/server/ServerConsole.tsx +++ b/resources/scripts/components/server/ServerConsole.tsx @@ -1,4 +1,5 @@ import React, { lazy, useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; import { ServerContext } from '@/state/server'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons'; @@ -61,6 +62,9 @@ export default () => { return ( + + {server.name} | Console +

diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx index 669f04e84..d75fb2a7d 100644 --- a/resources/scripts/components/server/backups/BackupContainer.tsx +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; import Spinner from '@/components/elements/Spinner'; import getServerBackups from '@/api/server/backups/getServerBackups'; import useServer from '@/plugins/useServer'; @@ -18,6 +19,7 @@ export default () => { const [ loading, setLoading ] = useState(true); const backups = ServerContext.useStoreState(state => state.backups.data); + const server = ServerContext.useStoreState(state => state.server.data!); const setBackups = ServerContext.useStoreActions(actions => actions.backups.setBackups); useEffect(() => { @@ -37,6 +39,9 @@ export default () => { return ( + + {server.name} | Backups + {!backups.length ?

diff --git a/resources/scripts/components/server/databases/DatabasesContainer.tsx b/resources/scripts/components/server/databases/DatabasesContainer.tsx index 486072598..462d90fb1 100644 --- a/resources/scripts/components/server/databases/DatabasesContainer.tsx +++ b/resources/scripts/components/server/databases/DatabasesContainer.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; import getServerDatabases from '@/api/server/getServerDatabases'; import { ServerContext } from '@/state/server'; import { httpErrorToHuman } from '@/api/http'; @@ -19,6 +20,7 @@ export default () => { const [ loading, setLoading ] = useState(true); const databases = ServerContext.useStoreState(state => state.databases.data); + const servername = ServerContext.useStoreState(state => state.server.data.name); const setDatabases = ServerContext.useStoreActions(state => state.databases.setDatabases); useEffect(() => { @@ -36,6 +38,9 @@ export default () => { return ( + + {servername} | Databases + {(!databases.length && loading) ? diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index def0944c2..9a1b68912 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from 'react'; +import { Helmet } from 'react-helmet'; import { httpErrorToHuman } from '@/api/http'; import { CSSTransition } from 'react-transition-group'; import Spinner from '@/components/elements/Spinner'; @@ -26,6 +27,8 @@ export default () => { const { id } = useServer(); const { hash } = useLocation(); const { data: files, error, mutate } = useFileManagerSwr(); + + const servername = ServerContext.useStoreState(state => state.server.data.name); const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory); const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles); @@ -42,6 +45,9 @@ export default () => { return ( + + {servername} | File Manager + { !files ? diff --git a/resources/scripts/components/server/network/NetworkContainer.tsx b/resources/scripts/components/server/network/NetworkContainer.tsx index 1723f9352..4470f681e 100644 --- a/resources/scripts/components/server/network/NetworkContainer.tsx +++ b/resources/scripts/components/server/network/NetworkContainer.tsx @@ -1,4 +1,6 @@ import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; +import { ServerContext } from '@/state/server'; import tw from 'twin.macro'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faNetworkWired } from '@fortawesome/free-solid-svg-icons'; @@ -28,6 +30,8 @@ const NetworkContainer = () => { const [ loading, setLoading ] = useState(false); const { data, error, mutate } = useSWR(uuid, key => getServerAllocations(key), { initialData: allocations }); + const servername = ServerContext.useStoreState(state => state.server.data.name); + const setPrimaryAllocation = (id: number) => { clearFlashes('server:network'); @@ -61,6 +65,9 @@ const NetworkContainer = () => { return ( + + {servername} | Network + {!data ? : diff --git a/resources/scripts/components/server/schedules/ScheduleContainer.tsx b/resources/scripts/components/server/schedules/ScheduleContainer.tsx index 0e4ff6bd2..2ee4038cc 100644 --- a/resources/scripts/components/server/schedules/ScheduleContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleContainer.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; import getServerSchedules from '@/api/server/schedules/getServerSchedules'; import { ServerContext } from '@/state/server'; import Spinner from '@/components/elements/Spinner'; @@ -22,6 +23,7 @@ export default ({ match, history }: RouteComponentProps) => { const [ visible, setVisible ] = useState(false); const schedules = ServerContext.useStoreState(state => state.schedules.data); + const servername = ServerContext.useStoreState(state => state.server.data.name); const setSchedules = ServerContext.useStoreActions(actions => actions.schedules.setSchedules); useEffect(() => { @@ -37,6 +39,9 @@ export default ({ match, history }: RouteComponentProps) => { return ( + + {servername} | Schedules + {(!schedules.length && loading) ? diff --git a/resources/scripts/components/server/settings/SettingsContainer.tsx b/resources/scripts/components/server/settings/SettingsContainer.tsx index e060fedf8..edaa3503f 100644 --- a/resources/scripts/components/server/settings/SettingsContainer.tsx +++ b/resources/scripts/components/server/settings/SettingsContainer.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Helmet } from 'react-helmet'; import TitledGreyBox from '@/components/elements/TitledGreyBox'; import { ServerContext } from '@/state/server'; import { useStoreState } from 'easy-peasy'; @@ -20,6 +21,9 @@ export default () => { return ( + + {server.name} | Settings +

diff --git a/resources/scripts/components/server/users/UsersContainer.tsx b/resources/scripts/components/server/users/UsersContainer.tsx index 55f60b449..0925e87df 100644 --- a/resources/scripts/components/server/users/UsersContainer.tsx +++ b/resources/scripts/components/server/users/UsersContainer.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; import { ServerContext } from '@/state/server'; import { Actions, useStoreActions, useStoreState } from 'easy-peasy'; import { ApplicationStore } from '@/state'; @@ -17,6 +18,7 @@ export default () => { const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); 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 permissions = useStoreState((state: ApplicationStore) => state.permissions.data); @@ -49,6 +51,9 @@ export default () => { return ( + + {servername} | Subusers + {!subusers.length ?

diff --git a/yarn.lock b/yarn.lock index 62c1da6fb..253b6cd86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5564,7 +5564,7 @@ react-fast-compare@^2.0.1: version "2.0.4" 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" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== @@ -5576,6 +5576,16 @@ react-google-recaptcha@^2.0.1: prop-types "^15.5.0" 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: version "4.12.21" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975" @@ -5643,6 +5653,11 @@ react-router@5.1.2: tiny-invariant "^1.0.2" 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: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" From 4c558a86628304b272a9736c54b9e09f36923567 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 29 Jul 2020 20:23:46 -0700 Subject: [PATCH 06/27] Fix date display for scheduled tasks; closes #2195 --- resources/scripts/components/server/schedules/ScheduleRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/scripts/components/server/schedules/ScheduleRow.tsx b/resources/scripts/components/server/schedules/ScheduleRow.tsx index ec23c6f10..514d50ac8 100644 --- a/resources/scripts/components/server/schedules/ScheduleRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleRow.tsx @@ -14,7 +14,7 @@ export default ({ schedule }: { schedule: Schedule }) => (

{schedule.name}

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'}

From 874d928a50c662c9ec0ff05f1ffb1321b1ce5307 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 29 Jul 2020 20:34:06 -0700 Subject: [PATCH 07/27] Correctly handle response from daemon for server stats; #2183 --- app/Transformers/Api/Client/StatsTransformer.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Transformers/Api/Client/StatsTransformer.php b/app/Transformers/Api/Client/StatsTransformer.php index 0fc1563a0..97989cc3a 100644 --- a/app/Transformers/Api/Client/StatsTransformer.php +++ b/app/Transformers/Api/Client/StatsTransformer.php @@ -27,11 +27,11 @@ class StatsTransformer extends BaseClientTransformer 'current_state' => Arr::get($data, 'state', 'stopped'), 'is_suspended' => Arr::get($data, 'suspended', false), 'resources' => [ - 'memory_bytes' => Arr::get($data, 'resources.memory_bytes', 0), - 'cpu_absolute' => Arr::get($data, 'resources.cpu_absolute', 0), - 'disk_bytes' => Arr::get($data, 'resources.disk_bytes', 0), - 'network_rx_bytes' => Arr::get($data, 'resources.network.rx_bytes', 0), - 'network_tx_bytes' => Arr::get($data, 'resources.network.tx_bytes', 0), + 'memory_bytes' => Arr::get($data, 'memory_bytes', 0), + 'cpu_absolute' => Arr::get($data, 'cpu_absolute', 0), + 'disk_bytes' => Arr::get($data, 'disk_bytes', 0), + 'network_rx_bytes' => Arr::get($data, 'network.rx_bytes', 0), + 'network_tx_bytes' => Arr::get($data, 'network.tx_bytes', 0), ], ]; } From 0fa90dd6bd812c33c1de73c0ed6d52bd737ca282 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 29 Jul 2020 22:02:00 -0700 Subject: [PATCH 08/27] Add listener for install start/end --- .../components/server/InstallListener.tsx | 26 +++++++++++++++++++ resources/scripts/plugins/useServer.ts | 5 ++-- resources/scripts/routers/ServerRouter.tsx | 4 ++- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 resources/scripts/components/server/InstallListener.tsx diff --git a/resources/scripts/components/server/InstallListener.tsx b/resources/scripts/components/server/InstallListener.tsx new file mode 100644 index 000000000..8bc85778a --- /dev/null +++ b/resources/scripts/components/server/InstallListener.tsx @@ -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; diff --git a/resources/scripts/plugins/useServer.ts b/resources/scripts/plugins/useServer.ts index 40fd93da1..8014ced58 100644 --- a/resources/scripts/plugins/useServer.ts +++ b/resources/scripts/plugins/useServer.ts @@ -1,9 +1,8 @@ -import { DependencyList } from 'react'; import { ServerContext } from '@/state/server'; import { Server } from '@/api/server/getServer'; -const useServer = (dependencies?: DependencyList): Server => { - return ServerContext.useStoreState(state => state.server.data!, [ dependencies ]); +const useServer = (dependencies?: any[] | undefined): Server => { + return ServerContext.useStoreState(state => state.server.data!, dependencies); }; export default useServer; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 9df270eaa..3fe87cca0 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -25,6 +25,7 @@ import useServer from '@/plugins/useServer'; import ScreenBlock from '@/components/screens/ScreenBlock'; import SubNavigation from '@/components/elements/SubNavigation'; import NetworkContainer from '@/components/server/network/NetworkContainer'; +import InstallListener from '@/components/server/InstallListener'; const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const { rootAdmin } = useStoreState(state => state.user.data!); @@ -98,6 +99,8 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
+ + {(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ? ) /> : <> - From b92c97060b10439f4a61cc1602fad16b5feeee7d Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 1 Aug 2020 18:48:58 -0700 Subject: [PATCH 09/27] Use a key that doesn't change to avoid re-render issues; closes #2203 --- resources/scripts/api/server/files/loadDirectory.ts | 2 +- resources/scripts/api/transformers.ts | 3 +-- .../components/server/files/FileDropdownMenu.tsx | 11 +++++++---- .../components/server/files/FileManagerContainer.tsx | 2 +- .../scripts/components/server/files/FileObjectRow.tsx | 2 +- .../components/server/files/NewDirectoryButton.tsx | 3 +-- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/resources/scripts/api/server/files/loadDirectory.ts b/resources/scripts/api/server/files/loadDirectory.ts index 7899d2216..77e44bce8 100644 --- a/resources/scripts/api/server/files/loadDirectory.ts +++ b/resources/scripts/api/server/files/loadDirectory.ts @@ -2,7 +2,7 @@ import http from '@/api/http'; import { rawDataToFileObject } from '@/api/transformers'; export interface FileObject { - uuid: string; + key: string; name: string; mode: string; size: number; diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index 4548c4b1e..6ac0ba1dd 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -1,7 +1,6 @@ import { Allocation } from '@/api/server/getServer'; import { FractalResponseData } from '@/api/http'; import { FileObject } from '@/api/server/files/loadDirectory'; -import v4 from 'uuid/v4'; export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({ id: data.attributes.id, @@ -13,7 +12,7 @@ export const rawDataToServerAllocation = (data: FractalResponseData): Allocation }); export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({ - uuid: v4(), + key: `${data.attributes.is_file ? 'file' : 'dir'}_${data.attributes.name}`, name: data.attributes.name, mode: data.attributes.mode, size: Number(data.attributes.size), diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx index 19fbe522a..e64dd3d84 100644 --- a/resources/scripts/components/server/files/FileDropdownMenu.tsx +++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { memo, useRef, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBoxOpen, @@ -29,6 +29,7 @@ import styled from 'styled-components/macro'; import useEventListener from '@/plugins/useEventListener'; import compressFiles from '@/api/server/files/compressFiles'; import decompressFiles from '@/api/server/files/decompressFiles'; +import isEqual from 'react-fast-compare'; type ModalType = 'rename' | 'move'; @@ -50,7 +51,7 @@ const Row = ({ icon, title, ...props }: RowProps) => ( ); -export default ({ file }: { file: FileObject }) => { +const FileDropdownMenu = ({ file }: { file: FileObject }) => { const onClickRef = useRef(null); const [ showSpinner, setShowSpinner ] = useState(false); const [ modal, setModal ] = useState(null); @@ -60,7 +61,7 @@ export default ({ file }: { file: FileObject }) => { const { clearAndAddHttpError, clearFlashes } = useFlash(); 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) { 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. // 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 => { mutate(); @@ -166,3 +167,5 @@ export default ({ file }: { file: FileObject }) => { ); }; + +export default memo(FileDropdownMenu, isEqual); diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index def0944c2..a6d6b89af 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -65,7 +65,7 @@ export default () => { } { sortFiles(files.slice(0, 250)).map(file => ( - + )) } diff --git a/resources/scripts/components/server/files/FileObjectRow.tsx b/resources/scripts/components/server/files/FileObjectRow.tsx index a78a83cc1..0a14aca8c 100644 --- a/resources/scripts/components/server/files/FileObjectRow.tsx +++ b/resources/scripts/components/server/files/FileObjectRow.tsx @@ -39,7 +39,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => { key={file.name} onContextMenu={e => { 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 })); }} > diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx index 9adbf57dd..27cfb15f7 100644 --- a/resources/scripts/components/server/files/NewDirectoryButton.tsx +++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx @@ -6,7 +6,6 @@ import Field from '@/components/elements/Field'; import { join } from 'path'; import { object, string } from 'yup'; import createDirectory from '@/api/server/files/createDirectory'; -import v4 from 'uuid/v4'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import { mutate } from 'swr'; @@ -24,7 +23,7 @@ const schema = object().shape({ }); const generateDirectoryData = (name: string): FileObject => ({ - uuid: v4(), + key: `dir_${name}`, name: name, mode: '0644', size: 0, From 0c7f118f45955638cabc4d1eb597648aa74a1e06 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 1 Aug 2020 19:44:50 -0700 Subject: [PATCH 10/27] add withFlash() context HOC --- .../server/files/RenameFileModal.tsx | 13 ++++++----- resources/scripts/hoc/withFlash.tsx | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 resources/scripts/hoc/withFlash.tsx diff --git a/resources/scripts/components/server/files/RenameFileModal.tsx b/resources/scripts/components/server/files/RenameFileModal.tsx index 155b45e9b..8ecbc9d91 100644 --- a/resources/scripts/components/server/files/RenameFileModal.tsx +++ b/resources/scripts/components/server/files/RenameFileModal.tsx @@ -9,23 +9,22 @@ import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import useServer from '@/plugins/useServer'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; -import useFlash from '@/plugins/useFlash'; +import withFlash, { WithFlashProps } from '@/hoc/withFlash'; interface FormikValues { name: string; } -type Props = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean }; +type OwnProps = WithFlashProps & RequiredModalProps & { files: string[]; useMoveTerminology?: boolean }; -export default ({ files, useMoveTerminology, ...props }: Props) => { +const RenameFileModal = ({ flash, files, useMoveTerminology, ...props }: OwnProps) => { const { uuid } = useServer(); const { mutate } = useFileManagerSwr(); - const { clearFlashes, clearAndAddHttpError } = useFlash(); const directory = ServerContext.useStoreState(state => state.files.directory); const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles); const submit = ({ name }: FormikValues, { setSubmitting }: FormikHelpers) => { - clearFlashes('files'); + flash.clearFlashes('files'); const len = name.split('/').length; if (files.length === 1) { @@ -51,7 +50,7 @@ export default ({ files, useMoveTerminology, ...props }: Props) => { .catch(error => { mutate(); setSubmitting(false); - clearAndAddHttpError({ key: 'files', error }); + flash.clearAndAddHttpError({ key: 'files', error }); }) .then(() => props.onDismissed()); }; @@ -96,3 +95,5 @@ export default ({ files, useMoveTerminology, ...props }: Props) => { ); }; + +export default withFlash(RenameFileModal); diff --git a/resources/scripts/hoc/withFlash.tsx b/resources/scripts/hoc/withFlash.tsx new file mode 100644 index 000000000..4a3f008f4 --- /dev/null +++ b/resources/scripts/hoc/withFlash.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import useFlash from '@/plugins/useFlash'; +import { Actions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; + +export interface WithFlashProps { + flash: Actions['flashes']; +} + +function withFlash (Component: React.ComponentType): React.ComponentType { + return (props: TOwnProps) => { + const { addError, addFlash, clearFlashes, clearAndAddHttpError } = useFlash(); + + return ( + + ); + }; +} + +export default withFlash; From c58348735d85c0be774a44a77eece89307492a0d Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 1 Aug 2020 19:49:38 -0700 Subject: [PATCH 11/27] Avoid double-click double-submit issues in modals; closes #2199 --- .../components/server/backups/CreateBackupButton.tsx | 8 ++------ .../components/server/schedules/EditScheduleModal.tsx | 2 +- .../components/server/schedules/TaskDetailsModal.tsx | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/resources/scripts/components/server/backups/CreateBackupButton.tsx b/resources/scripts/components/server/backups/CreateBackupButton.tsx index 7a04f1041..3d7834fa9 100644 --- a/resources/scripts/components/server/backups/CreateBackupButton.tsx +++ b/resources/scripts/components/server/backups/CreateBackupButton.tsx @@ -49,7 +49,7 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
-
@@ -94,11 +94,7 @@ export default () => { ignored: string(), })} > - setVisible(false)} - /> + setVisible(false)}/> }
-
diff --git a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx index 3829d724d..b8b102ba0 100644 --- a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx +++ b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx @@ -32,7 +32,7 @@ interface Values { } const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => { - const { values: { action }, setFieldValue, setFieldTouched } = useFormikContext(); + const { values: { action }, setFieldValue, setFieldTouched, isSubmitting } = useFormikContext(); useEffect(() => { setFieldValue('payload', action === 'power' ? 'start' : ''); @@ -94,7 +94,7 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => { />
-
From a9666138907e06f531973c65d2576e0b6b68935b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 1 Aug 2020 19:52:13 -0700 Subject: [PATCH 12/27] Fix task edit modal not filling the payload correctly --- .../components/server/schedules/TaskDetailsModal.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx index b8b102ba0..00457a4ec 100644 --- a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx +++ b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx @@ -32,11 +32,16 @@ interface Values { } const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => { - const { values: { action }, setFieldValue, setFieldTouched, isSubmitting } = useFormikContext(); + const { values: { action }, initialValues, setFieldValue, setFieldTouched, isSubmitting } = useFormikContext(); useEffect(() => { - setFieldValue('payload', action === 'power' ? 'start' : ''); - setFieldTouched('payload', false); + if (action !== initialValues.action) { + setFieldValue('payload', action === 'power' ? 'start' : ''); + setFieldTouched('payload', false); + } else { + setFieldValue('payload', initialValues.payload); + setFieldTouched('payload', false); + } }, [ action ]); return ( From dd381f65a92a094dbadb25697b3a6cddf1d6abc0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 1 Aug 2020 20:06:17 -0700 Subject: [PATCH 13/27] Don't try to be fancy, just pain --- .../server/files/RenameFileModal.tsx | 13 ++++++----- resources/scripts/hoc/withFlash.tsx | 23 ------------------- 2 files changed, 7 insertions(+), 29 deletions(-) delete mode 100644 resources/scripts/hoc/withFlash.tsx diff --git a/resources/scripts/components/server/files/RenameFileModal.tsx b/resources/scripts/components/server/files/RenameFileModal.tsx index 8ecbc9d91..fb3c0620d 100644 --- a/resources/scripts/components/server/files/RenameFileModal.tsx +++ b/resources/scripts/components/server/files/RenameFileModal.tsx @@ -9,22 +9,23 @@ import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import useServer from '@/plugins/useServer'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; -import withFlash, { WithFlashProps } from '@/hoc/withFlash'; +import useFlash from '@/plugins/useFlash'; interface FormikValues { name: string; } -type OwnProps = WithFlashProps & RequiredModalProps & { files: string[]; useMoveTerminology?: boolean }; +type OwnProps = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean }; -const RenameFileModal = ({ flash, files, useMoveTerminology, ...props }: OwnProps) => { +const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => { const { uuid } = useServer(); const { mutate } = useFileManagerSwr(); + const { clearFlashes, clearAndAddHttpError } = useFlash(); const directory = ServerContext.useStoreState(state => state.files.directory); const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles); const submit = ({ name }: FormikValues, { setSubmitting }: FormikHelpers) => { - flash.clearFlashes('files'); + clearFlashes('files'); const len = name.split('/').length; if (files.length === 1) { @@ -50,7 +51,7 @@ const RenameFileModal = ({ flash, files, useMoveTerminology, ...props }: OwnProp .catch(error => { mutate(); setSubmitting(false); - flash.clearAndAddHttpError({ key: 'files', error }); + clearAndAddHttpError({ key: 'files', error }); }) .then(() => props.onDismissed()); }; @@ -96,4 +97,4 @@ const RenameFileModal = ({ flash, files, useMoveTerminology, ...props }: OwnProp ); }; -export default withFlash(RenameFileModal); +export default RenameFileModal; diff --git a/resources/scripts/hoc/withFlash.tsx b/resources/scripts/hoc/withFlash.tsx deleted file mode 100644 index 4a3f008f4..000000000 --- a/resources/scripts/hoc/withFlash.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import useFlash from '@/plugins/useFlash'; -import { Actions } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; - -export interface WithFlashProps { - flash: Actions['flashes']; -} - -function withFlash (Component: React.ComponentType): React.ComponentType { - return (props: TOwnProps) => { - const { addError, addFlash, clearFlashes, clearAndAddHttpError } = useFlash(); - - return ( - - ); - }; -} - -export default withFlash; From d3316f61d7d0f61890ca4d7c3bb706cf3fe7c2ff Mon Sep 17 00:00:00 2001 From: Charles Morgan Date: Sat, 1 Aug 2020 23:49:00 -0400 Subject: [PATCH 14/27] Titles on index / account pages Also changed to use `const { ..., name: serverName } = useServer();` where feasible --- .../scripts/components/dashboard/AccountApiContainer.tsx | 7 ++++++- .../components/dashboard/AccountOverviewContainer.tsx | 7 +++++++ .../scripts/components/dashboard/DashboardContainer.tsx | 6 ++++++ .../scripts/components/server/backups/BackupContainer.tsx | 5 ++--- .../components/server/databases/DatabasesContainer.tsx | 5 ++--- .../components/server/files/FileManagerContainer.tsx | 5 ++--- .../scripts/components/server/network/NetworkContainer.tsx | 7 ++----- .../components/server/schedules/ScheduleContainer.tsx | 5 ++--- .../scripts/components/server/users/UsersContainer.tsx | 2 +- 9 files changed, 30 insertions(+), 19 deletions(-) diff --git a/resources/scripts/components/dashboard/AccountApiContainer.tsx b/resources/scripts/components/dashboard/AccountApiContainer.tsx index f3ceb66f0..c80a51a20 100644 --- a/resources/scripts/components/dashboard/AccountApiContainer.tsx +++ b/resources/scripts/components/dashboard/AccountApiContainer.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; import ContentBox from '@/components/elements/ContentBox'; import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm'; 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 ConfirmationModal from '@/components/elements/ConfirmationModal'; import deleteApiKey from '@/api/account/deleteApiKey'; -import { Actions, useStoreActions } from 'easy-peasy'; +import { Actions, useStoreActions, useStoreState } from 'easy-peasy'; import { ApplicationStore } from '@/state'; import FlashMessageRender from '@/components/FlashMessageRender'; import { httpErrorToHuman } from '@/api/http'; @@ -21,6 +22,7 @@ export default () => { const [ keys, setKeys ] = useState([]); const [ loading, setLoading ] = useState(true); const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); + const name = useStoreState((state: ApplicationStore) => state.settings.data!.name); useEffect(() => { clearFlashes('account'); @@ -49,6 +51,9 @@ export default () => { return ( + + {name} | API +
diff --git a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx index e98ddd4a6..d495400b4 100644 --- a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx +++ b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx @@ -1,4 +1,6 @@ import * as React from 'react'; +import { Helmet } from 'react-helmet'; +import { ApplicationStore } from '@/state'; import ContentBox from '@/components/elements/ContentBox'; import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm'; import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm'; @@ -7,6 +9,7 @@ import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; import { breakpoint } from '@/theme'; import styled from 'styled-components/macro'; +import { useStoreState } from 'easy-peasy'; const Container = styled.div` ${tw`flex flex-wrap my-10`}; @@ -25,8 +28,12 @@ const Container = styled.div` `; export default () => { + const name = useStoreState((state: ApplicationStore) => state.settings.data!.name); return ( + + {name} | Account Overview + diff --git a/resources/scripts/components/dashboard/DashboardContainer.tsx b/resources/scripts/components/dashboard/DashboardContainer.tsx index a20472604..1e1e702ca 100644 --- a/resources/scripts/components/dashboard/DashboardContainer.tsx +++ b/resources/scripts/components/dashboard/DashboardContainer.tsx @@ -1,5 +1,7 @@ import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; import { Server } from '@/api/server/getServer'; +import { ApplicationStore } from '@/state'; import getServers from '@/api/getServers'; import ServerRow from '@/components/dashboard/ServerRow'; import Spinner from '@/components/elements/Spinner'; @@ -18,6 +20,7 @@ export default () => { const [ page, setPage ] = useState(1); const { rootAdmin } = useStoreState(state => state.user.data!); const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState('show_all_servers', false); + const name = useStoreState((state: ApplicationStore) => state.settings.data!.name); const { data: servers, error } = useSWR>( [ '/api/client/servers', showOnlyAdmin, page ], @@ -31,6 +34,9 @@ export default () => { return ( + + {name} | Dashboard + {rootAdmin &&

diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx index c8abddcae..bcead7abb 100644 --- a/resources/scripts/components/server/backups/BackupContainer.tsx +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -14,12 +14,11 @@ import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; export default () => { - const { uuid, featureLimits } = useServer(); + const { uuid, featureLimits, name: serverName } = useServer(); const { addError, clearFlashes } = useFlash(); const [ loading, setLoading ] = useState(true); const backups = ServerContext.useStoreState(state => state.backups.data); - const server = ServerContext.useStoreState(state => state.server.data!); const setBackups = ServerContext.useStoreActions(actions => actions.backups.setBackups); useEffect(() => { @@ -40,7 +39,7 @@ export default () => { return ( - {server.name} | Backups + {serverName} | Backups {!backups.length ? diff --git a/resources/scripts/components/server/databases/DatabasesContainer.tsx b/resources/scripts/components/server/databases/DatabasesContainer.tsx index 462d90fb1..922f0a364 100644 --- a/resources/scripts/components/server/databases/DatabasesContainer.tsx +++ b/resources/scripts/components/server/databases/DatabasesContainer.tsx @@ -15,12 +15,11 @@ import tw from 'twin.macro'; import Fade from '@/components/elements/Fade'; export default () => { - const { uuid, featureLimits } = useServer(); + const { uuid, featureLimits, name: serverName } = useServer(); const { addError, clearFlashes } = useFlash(); const [ loading, setLoading ] = useState(true); const databases = ServerContext.useStoreState(state => state.databases.data); - const servername = ServerContext.useStoreState(state => state.server.data.name); const setDatabases = ServerContext.useStoreActions(state => state.databases.setDatabases); useEffect(() => { @@ -39,7 +38,7 @@ export default () => { return ( - {servername} | Databases + {serverName} | Databases {(!databases.length && loading) ? diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index 380a09513..d25ef3c1c 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -24,11 +24,10 @@ const sortFiles = (files: FileObject[]): FileObject[] => { }; export default () => { - const { id } = useServer(); + const { id, name: serverName } = useServer(); const { hash } = useLocation(); const { data: files, error, mutate } = useFileManagerSwr(); - const servername = ServerContext.useStoreState(state => state.server.data.name); const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory); const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles); @@ -46,7 +45,7 @@ export default () => { return ( - {servername} | File Manager + {serverName} | File Manager { diff --git a/resources/scripts/components/server/network/NetworkContainer.tsx b/resources/scripts/components/server/network/NetworkContainer.tsx index 4470f681e..a330685b1 100644 --- a/resources/scripts/components/server/network/NetworkContainer.tsx +++ b/resources/scripts/components/server/network/NetworkContainer.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; -import { ServerContext } from '@/state/server'; import tw from 'twin.macro'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faNetworkWired } from '@fortawesome/free-solid-svg-icons'; @@ -25,13 +24,11 @@ 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 NetworkContainer = () => { - const { uuid, allocations } = useServer(); + const { uuid, allocations, name: serverName } = useServer(); const { clearFlashes, clearAndAddHttpError } = useFlash(); const [ loading, setLoading ] = useState(false); const { data, error, mutate } = useSWR(uuid, key => getServerAllocations(key), { initialData: allocations }); - const servername = ServerContext.useStoreState(state => state.server.data.name); - const setPrimaryAllocation = (id: number) => { clearFlashes('server:network'); @@ -66,7 +63,7 @@ const NetworkContainer = () => { return ( - {servername} | Network + {serverName} | Network {!data ? diff --git a/resources/scripts/components/server/schedules/ScheduleContainer.tsx b/resources/scripts/components/server/schedules/ScheduleContainer.tsx index 2ee4038cc..77e31b590 100644 --- a/resources/scripts/components/server/schedules/ScheduleContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleContainer.tsx @@ -17,13 +17,12 @@ import GreyRowBox from '@/components/elements/GreyRowBox'; import Button from '@/components/elements/Button'; export default ({ match, history }: RouteComponentProps) => { - const { uuid } = useServer(); + const { uuid, name: serverName } = useServer(); const { clearFlashes, addError } = useFlash(); const [ loading, setLoading ] = useState(true); const [ visible, setVisible ] = useState(false); const schedules = ServerContext.useStoreState(state => state.schedules.data); - const servername = ServerContext.useStoreState(state => state.server.data.name); const setSchedules = ServerContext.useStoreActions(actions => actions.schedules.setSchedules); useEffect(() => { @@ -40,7 +39,7 @@ export default ({ match, history }: RouteComponentProps) => { return ( - {servername} | Schedules + {serverName} | Schedules {(!schedules.length && loading) ? diff --git a/resources/scripts/components/server/users/UsersContainer.tsx b/resources/scripts/components/server/users/UsersContainer.tsx index 0925e87df..a58d9e904 100644 --- a/resources/scripts/components/server/users/UsersContainer.tsx +++ b/resources/scripts/components/server/users/UsersContainer.tsx @@ -18,7 +18,7 @@ export default () => { const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const subusers = ServerContext.useStoreState(state => state.subusers.data); - const servername = ServerContext.useStoreState(state => state.server.data.name); + const servername = ServerContext.useStoreState(state => state.server.data!.name); const setSubusers = ServerContext.useStoreActions(actions => actions.subusers.setSubusers); const permissions = useStoreState((state: ApplicationStore) => state.permissions.data); From b52fc0b4d9532d967b677a4bb0cbbb7db53416db Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 1 Aug 2020 21:08:35 -0700 Subject: [PATCH 15/27] Fix recaptcha handling during login & password reset flows; closes #2064 --- package.json | 5 +- .../api/auth/requestPasswordResetEmail.ts | 4 +- .../auth/ForgotPasswordContainer.tsx | 40 +++- .../components/auth/LoginContainer.tsx | 187 ++++++++---------- resources/scripts/state/flashes.ts | 2 +- routes/auth.php | 2 +- yarn.lock | 27 +-- 7 files changed, 131 insertions(+), 136 deletions(-) diff --git a/package.json b/package.json index 3a81f98fa..0ca8c2e9b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "@fortawesome/fontawesome-svg-core": "1.2.19", "@fortawesome/free-solid-svg-icons": "^5.9.0", "@fortawesome/react-fontawesome": "0.1.4", - "@types/react-google-recaptcha": "^1.1.1", "axios": "^0.19.2", "ayu-ace": "^2.0.4", "brace": "^0.11.1", @@ -23,15 +22,15 @@ "path": "^0.12.7", "query-string": "^6.7.0", "react": "^16.13.1", - "react-ga": "^3.1.2", "react-dom": "npm:@hot-loader/react-dom", "react-fast-compare": "^3.2.0", - "react-google-recaptcha": "^2.0.1", + "react-ga": "^3.1.2", "react-hot-loader": "^4.12.21", "react-i18next": "^11.2.1", "react-redux": "^7.1.0", "react-router-dom": "^5.1.2", "react-transition-group": "^4.4.1", + "reaptcha": "^1.7.2", "sockette": "^2.0.6", "styled-components": "^5.1.1", "styled-components-breakpoint": "^3.0.0-preview.20", diff --git a/resources/scripts/api/auth/requestPasswordResetEmail.ts b/resources/scripts/api/auth/requestPasswordResetEmail.ts index d70139899..2168160c2 100644 --- a/resources/scripts/api/auth/requestPasswordResetEmail.ts +++ b/resources/scripts/api/auth/requestPasswordResetEmail.ts @@ -1,8 +1,8 @@ import http from '@/api/http'; -export default (email: string): Promise => { +export default (email: string, recaptchaData?: string): Promise => { 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 || '')) .catch(reject); }); diff --git a/resources/scripts/components/auth/ForgotPasswordContainer.tsx b/resources/scripts/components/auth/ForgotPasswordContainer.tsx index dbd4ed469..82bd5e5ff 100644 --- a/resources/scripts/components/auth/ForgotPasswordContainer.tsx +++ b/resources/scripts/components/auth/ForgotPasswordContainer.tsx @@ -1,27 +1,40 @@ import * as React from 'react'; +import { useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; import { httpErrorToHuman } from '@/api/http'; import LoginFormContainer from '@/components/auth/LoginFormContainer'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; +import { useStoreState } from 'easy-peasy'; import Field from '@/components/elements/Field'; import { Formik, FormikHelpers } from 'formik'; import { object, string } from 'yup'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; +import Reaptcha from 'reaptcha'; +import useFlash from '@/plugins/useFlash'; interface Values { email: string; } export default () => { - const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + const ref = useRef(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) => { - setSubmitting(true); 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 => { resetForm(); addFlash({ type: 'success', title: 'Success', message: response }); @@ -42,7 +55,7 @@ export default () => { .required('A valid email address must be provided to continue.'), })} > - {({ isSubmitting }) => ( + {({ isSubmitting, setSubmitting, submitForm }) => ( { Send Email

+ {recaptchaEnabled && + { + setToken(response); + submitForm(); + }} + onExpire={() => { + setSubmitting(false); + setToken(''); + }} + /> + }
; - addFlash: ActionCreator; +interface Values { + username: string; + password: string; } -const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps) => { - const ref = useRef(null); - const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha); +const LoginContainer = ({ history }: RouteComponentProps) => { + const ref = useRef(null); + const [ token, setToken ] = useState(''); - const submit = (e: React.FormEvent) => { - e.preventDefault(); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha); - if (ref.current && !values.recaptchaData) { - return ref.current.execute(); + const onSubmit = (values: Values, { setSubmitting }: FormikHelpers) => { + 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); - }; - - return ( - - {ref.current && ref.current.render()} - - -
- -
-
- -
- {recaptchaEnabled && - { - ref.current && ref.current.reset(); - setFieldValue('recaptchaData', token); - submitForm(); - }} - onExpired={() => setFieldValue('recaptchaData', null)} - /> - } -
- - Forgot password? - -
-
-
- ); -}; - -const EnhancedForm = withFormik({ - 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) + login({ ...values, recaptchaData: token }) .then(response => { if (response.complete) { // @ts-ignore @@ -107,26 +41,75 @@ const EnhancedForm = withFormik({ return; } - props.history.replace('/auth/login/checkpoint', { token: response.confirmationToken }); + 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) }); + clearAndAddHttpError({ error }); }); - }, -})(LoginContainer); - -export default (props: RouteComponentProps) => { - const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + }; return ( - + + {({ isSubmitting, setSubmitting, submitForm }) => ( + + +
+ +
+
+ +
+ {recaptchaEnabled && + { + setToken(response); + submitForm(); + }} + onExpire={() => { + setSubmitting(false); + setToken(''); + }} + /> + } +
+ + Forgot password? + +
+
+ )} +
); }; + +export default LoginContainer; diff --git a/resources/scripts/state/flashes.ts b/resources/scripts/state/flashes.ts index 8e4fb258e..fb89a0a8d 100644 --- a/resources/scripts/state/flashes.ts +++ b/resources/scripts/state/flashes.ts @@ -6,7 +6,7 @@ export interface FlashStore { items: FlashMessage[]; addFlash: Action; addError: Action; - clearAndAddHttpError: Action; + clearAndAddHttpError: Action; clearFlashes: Action; } diff --git a/routes/auth.php b/routes/auth.php index a6038447b..4bdb72206 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -26,7 +26,7 @@ Route::group(['middleware' => 'guest'], function () { // Password reset routes. This endpoint is hit after going through // the forgot password routes to acquire a token (or after an account // 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. Route::fallback('LoginController@index'); diff --git a/yarn.lock b/yarn.lock index f20fef049..73e239aef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1013,12 +1013,6 @@ dependencies: "@types/react" "*" -"@types/react-google-recaptcha@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-1.1.1.tgz#7dd2a4dd15d38d8059a2753cd4a7e3485c9bb3ea" - dependencies: - "@types/react" "*" - "@types/react-native@*": version "0.60.2" resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.60.2.tgz#2dca78481a904419c2a5907288dd97d1090c6e3c" @@ -5399,7 +5393,7 @@ promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" -prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -5544,13 +5538,6 @@ rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-async-script@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.1.1.tgz#f481c6c5f094bf4b94a9d52da0d0dda2e1a74bdf" - dependencies: - hoist-non-react-statics "^3.3.0" - prop-types "^15.5.0" - "react-dom@npm:@hot-loader/react-dom": version "16.11.0" resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.11.0.tgz#c0b483923b289db5431516f56ee2a69448ebf9bd" @@ -5574,13 +5561,6 @@ react-ga@^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: - version "2.0.1" - resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8" - dependencies: - prop-types "^15.5.0" - react-async-script "^1.1.1" - react-hot-loader@^4.12.21: version "4.12.21" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975" @@ -5719,6 +5699,11 @@ readdirp@~3.4.0: dependencies: 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: version "2.1.7" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2" From a1f1e4294df6b70fb438b0d349396bd50a42a5d8 Mon Sep 17 00:00:00 2001 From: Charles Morgan Date: Sun, 2 Aug 2020 00:11:49 -0400 Subject: [PATCH 16/27] conflict fix --- package.json | 3 +-- yarn.lock | 17 +++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index b69ecba10..401bcc7da 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "@fortawesome/fontawesome-svg-core": "1.2.19", "@fortawesome/free-solid-svg-icons": "^5.9.0", "@fortawesome/react-fontawesome": "0.1.4", - "@types/react-google-recaptcha": "^1.1.1", "axios": "^0.19.2", "ayu-ace": "^2.0.4", "brace": "^0.11.1", @@ -23,7 +22,6 @@ "path": "^0.12.7", "query-string": "^6.7.0", "react": "^16.13.1", - "react-ga": "^3.1.2", "react-dom": "npm:@hot-loader/react-dom", "react-fast-compare": "^3.2.0", "react-google-recaptcha": "^2.0.1", @@ -33,6 +31,7 @@ "react-redux": "^7.1.0", "react-router-dom": "^5.1.2", "react-transition-group": "^4.4.1", + "reaptcha": "^1.7.2", "sockette": "^2.0.6", "styled-components": "^5.1.1", "styled-components-breakpoint": "^3.0.0-preview.20", diff --git a/yarn.lock b/yarn.lock index a717a0a9b..0bba5d32b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1013,9 +1013,10 @@ dependencies: "@types/react" "*" -"@types/react-google-recaptcha@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-1.1.1.tgz#7dd2a4dd15d38d8059a2753cd4a7e3485c9bb3ea" +"@types/react-helmet@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.0.0.tgz#5b74e44a12662ffb12d1c97ee702cf4e220958cf" + integrity sha512-NBMPAxgjpaMooXa51cU1BTgrX6T+hQbMiLm77JhBbfOzPQea3RB5rNpPOD5xGWHIVpGXHd59cltEzIq0qglGcQ== dependencies: "@types/react" "*" @@ -5569,11 +5570,6 @@ react-fast-compare@^3.1.1, react-fast-compare@^3.2.0: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" 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: version "2.0.1" resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8" @@ -5734,6 +5730,11 @@ readdirp@~3.4.0: dependencies: 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: version "2.1.7" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz#1ace2e02c286d78abcd01fd92bfe8097ab0602c2" From 9387be3b0d277a0cdb985d506ff65f81caad6853 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 1 Aug 2020 21:25:28 -0700 Subject: [PATCH 17/27] Fix permissions on subuser rows --- .../components/server/users/UserRow.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/resources/scripts/components/server/users/UserRow.tsx b/resources/scripts/components/server/users/UserRow.tsx index 165d0f9c7..346b083e1 100644 --- a/resources/scripts/components/server/users/UserRow.tsx +++ b/resources/scripts/components/server/users/UserRow.tsx @@ -51,17 +51,18 @@ export default ({ subuser }: Props) => {

Permissions

- + + {subuser.uuid !== uuid && + + } + From 26704a2d5f8873f6bcf24f9214e263470b6d666a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 3 Aug 2020 20:58:15 -0700 Subject: [PATCH 18/27] Clear reinstall messages when mounting; closes #2213 --- .../components/server/settings/ReinstallServerBox.tsx | 6 +++++- yarn.lock | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/scripts/components/server/settings/ReinstallServerBox.tsx b/resources/scripts/components/server/settings/ReinstallServerBox.tsx index eef96c16c..1b7b44de7 100644 --- a/resources/scripts/components/server/settings/ReinstallServerBox.tsx +++ b/resources/scripts/components/server/settings/ReinstallServerBox.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { ServerContext } from '@/state/server'; import TitledGreyBox from '@/components/elements/TitledGreyBox'; import ConfirmationModal from '@/components/elements/ConfirmationModal'; @@ -37,6 +37,10 @@ export default () => { }); }; + useEffect(() => { + clearFlashes(); + }, []); + return ( Date: Tue, 4 Aug 2020 20:34:44 -0700 Subject: [PATCH 19/27] Return egg "done" checks as an array rather than a string --- app/Services/Eggs/EggConfigurationService.php | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/Services/Eggs/EggConfigurationService.php b/app/Services/Eggs/EggConfigurationService.php index 3d98cc33c..2659259b0 100644 --- a/app/Services/Eggs/EggConfigurationService.php +++ b/app/Services/Eggs/EggConfigurationService.php @@ -51,12 +51,30 @@ class EggConfigurationService ); 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), '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 array_filter([ + 'done' => is_string($done) ? [$done] : $done, + 'user_interaction' => Arr::get($startup, 'userInteraction'), + ], function ($datum) { + return ! is_null($datum); + }); + } + /** * Converts a legacy stop string into a new generation stop option for a server. * From c91c02f6a84936aab352960641daafd4bc139567 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 4 Aug 2020 20:38:24 -0700 Subject: [PATCH 20/27] Fix for struct in Go --- app/Services/Eggs/EggConfigurationService.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/Services/Eggs/EggConfigurationService.php b/app/Services/Eggs/EggConfigurationService.php index 2659259b0..9d30f8a86 100644 --- a/app/Services/Eggs/EggConfigurationService.php +++ b/app/Services/Eggs/EggConfigurationService.php @@ -67,12 +67,11 @@ class EggConfigurationService { $done = Arr::get($startup, 'done'); - return array_filter([ + return [ 'done' => is_string($done) ? [$done] : $done, - 'user_interaction' => Arr::get($startup, 'userInteraction'), - ], function ($datum) { - return ! is_null($datum); - }); + 'user_interaction' => Arr::get($startup, 'userInteraction') ?? [], + 'strip_ansi' => Arr::get($startup, 'strip_ansi') ?? false, + ]; } /** From d1a28051f9dab52f0880134af25dbec96ab8f181 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 4 Aug 2020 20:39:18 -0700 Subject: [PATCH 21/27] Support userInteraction and user_interaction because who needs this to be maintainable in the future... --- app/Services/Eggs/EggConfigurationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/Eggs/EggConfigurationService.php b/app/Services/Eggs/EggConfigurationService.php index 9d30f8a86..6f4eae689 100644 --- a/app/Services/Eggs/EggConfigurationService.php +++ b/app/Services/Eggs/EggConfigurationService.php @@ -69,7 +69,7 @@ class EggConfigurationService return [ 'done' => is_string($done) ? [$done] : $done, - 'user_interaction' => Arr::get($startup, 'userInteraction') ?? [], + 'user_interaction' => Arr::get($startup, 'userInteraction') ?? Arr::get($startup, 'user_interaction') ?? [], 'strip_ansi' => Arr::get($startup, 'strip_ansi') ?? false, ]; } From 95e8492c5dea75f62562ce333d09c6fe3d7b4966 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 6 Aug 2020 20:25:35 -0700 Subject: [PATCH 22/27] What the heck are these abysmal timeouts; closes #2223 --- app/Repositories/Wings/DaemonFileRepository.php | 3 +++ config/pterodactyl.php | 4 ++-- resources/scripts/api/server/files/compressFiles.ts | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/Repositories/Wings/DaemonFileRepository.php b/app/Repositories/Wings/DaemonFileRepository.php index 177f22afd..553e39d24 100644 --- a/app/Repositories/Wings/DaemonFileRepository.php +++ b/app/Repositories/Wings/DaemonFileRepository.php @@ -230,6 +230,9 @@ class DaemonFileRepository extends DaemonRepository 'root' => $root ?? '/', '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) { diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 70014bc0a..b37790cbc 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -85,8 +85,8 @@ return [ | Configure the timeout to be used for Guzzle connections here. */ 'guzzle' => [ - 'timeout' => env('GUZZLE_TIMEOUT', 5), - 'connect_timeout' => env('GUZZLE_CONNECT_TIMEOUT', 3), + 'timeout' => env('GUZZLE_TIMEOUT', 30), + 'connect_timeout' => env('GUZZLE_CONNECT_TIMEOUT', 10), ], /* diff --git a/resources/scripts/api/server/files/compressFiles.ts b/resources/scripts/api/server/files/compressFiles.ts index 0554c7fd9..4204f0884 100644 --- a/resources/scripts/api/server/files/compressFiles.ts +++ b/resources/scripts/api/server/files/compressFiles.ts @@ -4,8 +4,8 @@ import { rawDataToFileObject } from '@/api/transformers'; export default async (uuid: string, directory: string, files: string[]): Promise => { const { data } = await http.post(`/api/client/servers/${uuid}/files/compress`, { root: directory, files }, { - timeout: 300000, - timeoutErrorMessage: 'It looks like this archive is taking a long time to generate. It will appear when completed.', + timeout: 60000, + timeoutErrorMessage: 'It looks like this archive is taking a long time to generate. It will appear once completed.', }); return rawDataToFileObject(data); From 14c587eabea5097f9ab5e31530998ed6769f938c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 6 Aug 2020 20:33:17 -0700 Subject: [PATCH 23/27] Correctly inject new directory into file manager --- .../server/files/NewDirectoryButton.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx index 27cfb15f7..0a6a2b07f 100644 --- a/resources/scripts/components/server/files/NewDirectoryButton.tsx +++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx @@ -8,11 +8,10 @@ import { object, string } from 'yup'; import createDirectory from '@/api/server/files/createDirectory'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; -import { mutate } from 'swr'; import useServer from '@/plugins/useServer'; import { FileObject } from '@/api/server/files/loadDirectory'; -import { useLocation } from 'react-router'; import useFlash from '@/plugins/useFlash'; +import useFileManagerSwr from '@/plugins/useFileManagerSwr'; interface Values { directoryName: string; @@ -38,20 +37,16 @@ const generateDirectoryData = (name: string): FileObject => ({ export default () => { const { uuid } = useServer(); - const { hash } = useLocation(); const { clearAndAddHttpError } = useFlash(); const [ visible, setVisible ] = useState(false); + + const { mutate } = useFileManagerSwr(); const directory = ServerContext.useStoreState(state => state.files.directory); const submit = ({ directoryName }: Values, { setSubmitting }: FormikHelpers) => { createDirectory(uuid, directory, directoryName) - .then(() => { - mutate( - `${uuid}:files:${hash}`, - (data: FileObject[]) => [ ...data, generateDirectoryData(directoryName) ], - ); - setVisible(false); - }) + .then(() => mutate(data => [ ...data, generateDirectoryData(directoryName) ], false)) + .then(() => setVisible(false)) .catch(error => { console.error(error); setSubmitting(false); @@ -78,6 +73,7 @@ export default () => { >
Date: Wed, 12 Aug 2020 21:25:14 -0700 Subject: [PATCH 24/27] The first of our lovely sponsors --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 53c62f2b1..02899d449 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,17 @@ 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) +## 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) +> Ditch your overloaded server and see what dedicated Ryzen CPUs can do for your Minecraft community. + +#### [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. + ## 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). From c0f7c9bbf3ae3288fe77d1ee7603384caac993de Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 13 Aug 2020 20:29:46 -0700 Subject: [PATCH 25/27] Update README.md --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 02899d449..d788fb7c0 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,21 @@ I would like to extend my sincere thanks to the following sponsors for funding P in becoming a sponsor?](https://github.com/sponsors/DaneEveritt) #### [BloomVPS](https://bloomvps.com) -> Ditch your overloaded server and see what dedicated Ryzen CPUs can do for your Minecraft community. +> 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. +> 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 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). From 231ff0386c946915bcec332c243f31a66187ae07 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 13 Aug 2020 20:47:16 -0700 Subject: [PATCH 26/27] Fix kill button not showing up when restarting --- .../scripts/components/server/StopOrKillButton.tsx | 2 +- resources/scripts/components/server/events.ts | 10 ++++++++++ resources/scripts/plugins/Websocket.ts | 7 ------- 3 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 resources/scripts/components/server/events.ts diff --git a/resources/scripts/components/server/StopOrKillButton.tsx b/resources/scripts/components/server/StopOrKillButton.tsx index b9daed85b..fc8490655 100644 --- a/resources/scripts/components/server/StopOrKillButton.tsx +++ b/resources/scripts/components/server/StopOrKillButton.tsx @@ -8,7 +8,7 @@ const StopOrKillButton = ({ onPress }: { onPress: (action: PowerAction) => void const status = ServerContext.useStoreState(state => state.status.value); useEffect(() => { - setClicked(state => [ 'stopping' ].indexOf(status) < 0 ? false : state); + setClicked(status === 'stopping'); }, [ status ]); return ( diff --git a/resources/scripts/components/server/events.ts b/resources/scripts/components/server/events.ts new file mode 100644 index 000000000..4f4c35bde --- /dev/null +++ b/resources/scripts/components/server/events.ts @@ -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', +} diff --git a/resources/scripts/plugins/Websocket.ts b/resources/scripts/plugins/Websocket.ts index 0aa13769d..0f8150dcd 100644 --- a/resources/scripts/plugins/Websocket.ts +++ b/resources/scripts/plugins/Websocket.ts @@ -1,13 +1,6 @@ import Sockette from 'sockette'; import { EventEmitter } from 'events'; -export const SOCKET_EVENTS = [ - 'SOCKET_OPEN', - 'SOCKET_RECONNECT', - 'SOCKET_CLOSE', - 'SOCKET_ERROR', -]; - export class Websocket extends EventEmitter { // Timer instance for this socket. private timer: any = null; From 800b475ec5c6721134200808cb168b2ddff5785f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 13 Aug 2020 21:21:08 -0700 Subject: [PATCH 27/27] Respond with the actual error from wings if available; closes #2224 --- .../Connection/DaemonConnectionException.php | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/app/Exceptions/Http/Connection/DaemonConnectionException.php b/app/Exceptions/Http/Connection/DaemonConnectionException.php index 2eb7e93ca..e6765b8a6 100644 --- a/app/Exceptions/Http/Connection/DaemonConnectionException.php +++ b/app/Exceptions/Http/Connection/DaemonConnectionException.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Exceptions\Http\Connection; +use Illuminate\Support\Arr; use Illuminate\Http\Response; use GuzzleHttp\Exception\GuzzleException; use Pterodactyl\Exceptions\DisplayException; @@ -28,12 +29,28 @@ class DaemonConnectionException extends DisplayException $response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null; 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(), - ]), $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); } /**