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),
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/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),
],
];
}
diff --git a/package.json b/package.json
index 52d866a06..b69ecba10 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/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/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/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/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/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx
index d75fb2a7d..c8abddcae 100644
--- a/resources/scripts/components/server/backups/BackupContainer.tsx
+++ b/resources/scripts/components/server/backups/BackupContainer.tsx
@@ -57,7 +57,7 @@ export default () => {
}
{featureLimits.backups === 0 &&
-
+
Backups cannot be created for this server.
}
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)}/>
}
setVisible(true)}>
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 9a1b68912..380a09513 100644
--- a/resources/scripts/components/server/files/FileManagerContainer.tsx
+++ b/resources/scripts/components/server/files/FileManagerContainer.tsx
@@ -71,7 +71,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,
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/components/server/schedules/EditScheduleModal.tsx b/resources/scripts/components/server/schedules/EditScheduleModal.tsx
index 5793b875c..1cf9eea3a 100644
--- a/resources/scripts/components/server/schedules/EditScheduleModal.tsx
+++ b/resources/scripts/components/server/schedules/EditScheduleModal.tsx
@@ -65,7 +65,7 @@ const EditScheduleModal = ({ schedule, ...props }: Omit
-
+
{schedule ? 'Save changes' : 'Create schedule'}
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'}
diff --git a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx
index 3829d724d..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 } = 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 (
@@ -94,7 +99,7 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
/>
-
+
{isEditingTask ? 'Save Changes' : 'Create Task'}
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;
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/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..3fa5a9ff4 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';
@@ -25,6 +26,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!);
@@ -60,6 +62,10 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
};
}, [ match.params.id ]);
+ useEffect(() => {
+ ReactGA.pageview(location.pathname);
+ }, [ location.pathname ]);
+
return (
@@ -98,6 +104,8 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
+
+
{(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ?
)
/>
:
<>
-
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.
+