Merge branch 'develop' into pagetitles2

This commit is contained in:
Charles Morgan 2020-08-01 22:03:07 -05:00 committed by GitHub
commit d604a4a5f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 215 additions and 82 deletions

View File

@ -43,7 +43,7 @@ In addition to our standard nest of supported games, our community is constantly
## Credits ## Credits
This software would not be possible without the work of other open-source authors who provide tools such as: This software would not be possible without the work of other open-source authors who provide tools such as:
[Ace Editor](https://ace.c9.io), [AdminLTE](https://almsaeedstudio.com), [Animate.css](http://daneden.github.io/animate.css/), [AnsiUp](https://github.com/drudru/ansi_up), [Async.js](https://github.com/caolan/async), [Ace Editor](https://ace.c9.io), [AdminLTE](https://adminlte.io), [Animate.css](http://daneden.github.io/animate.css/), [AnsiUp](https://github.com/drudru/ansi_up), [Async.js](https://github.com/caolan/async),
[Bootstrap](http://getbootstrap.com), [Bootstrap Notify](http://bootstrap-notify.remabledesigns.com), [Chart.js](http://www.chartjs.org), [FontAwesome](http://fontawesome.io), [Bootstrap](http://getbootstrap.com), [Bootstrap Notify](http://bootstrap-notify.remabledesigns.com), [Chart.js](http://www.chartjs.org), [FontAwesome](http://fontawesome.io),
[FontAwesome Animations](https://github.com/l-lin/font-awesome-animation), [jQuery](http://jquery.com), [Laravel](https://laravel.com), [Lodash](https://lodash.com), [FontAwesome Animations](https://github.com/l-lin/font-awesome-animation), [jQuery](http://jquery.com), [Laravel](https://laravel.com), [Lodash](https://lodash.com),
[Select2](https://select2.github.io), [Socket.io](http://socket.io), [Socket.io File Upload](https://github.com/vote539/socketio-file-upload), [SweetAlert](http://t4t5.github.io/sweetalert), [Select2](https://select2.github.io), [Socket.io](http://socket.io), [Socket.io File Upload](https://github.com/vote539/socketio-file-upload), [SweetAlert](http://t4t5.github.io/sweetalert),

View File

@ -19,6 +19,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
'app:name' => 'required|string|max:255', 'app:name' => 'required|string|max:255',
'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2', 'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2',
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))], 'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
'app:analytics' => 'nullable|string',
]; ];
} }
@ -31,6 +32,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
'app:name' => 'Company Name', 'app:name' => 'Company Name',
'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication', 'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication',
'app:locale' => 'Default Language', 'app:locale' => 'Default Language',
'app:analytics' => 'Google Analytics',
]; ];
} }
} }

View File

@ -37,6 +37,7 @@ class AssetComposer
'enabled' => config('recaptcha.enabled', false), 'enabled' => config('recaptcha.enabled', false),
'siteKey' => config('recaptcha.website_key') ?? '', 'siteKey' => config('recaptcha.website_key') ?? '',
], ],
'analytics' => config('app.analytics') ?? '',
]); ]);
} }
} }

View File

@ -21,6 +21,7 @@ class SettingsServiceProvider extends ServiceProvider
protected $keys = [ protected $keys = [
'app:name', 'app:name',
'app:locale', 'app:locale',
'app:analytics',
'recaptcha:enabled', 'recaptcha:enabled',
'recaptcha:secret_key', 'recaptcha:secret_key',
'recaptcha:website_key', 'recaptcha:website_key',

View File

@ -27,11 +27,11 @@ class StatsTransformer extends BaseClientTransformer
'current_state' => Arr::get($data, 'state', 'stopped'), 'current_state' => Arr::get($data, 'state', 'stopped'),
'is_suspended' => Arr::get($data, 'suspended', false), 'is_suspended' => Arr::get($data, 'suspended', false),
'resources' => [ 'resources' => [
'memory_bytes' => Arr::get($data, 'resources.memory_bytes', 0), 'memory_bytes' => Arr::get($data, 'memory_bytes', 0),
'cpu_absolute' => Arr::get($data, 'resources.cpu_absolute', 0), 'cpu_absolute' => Arr::get($data, 'cpu_absolute', 0),
'disk_bytes' => Arr::get($data, 'resources.disk_bytes', 0), 'disk_bytes' => Arr::get($data, 'disk_bytes', 0),
'network_rx_bytes' => Arr::get($data, 'resources.network.rx_bytes', 0), 'network_rx_bytes' => Arr::get($data, 'network.rx_bytes', 0),
'network_tx_bytes' => Arr::get($data, 'resources.network.tx_bytes', 0), 'network_tx_bytes' => Arr::get($data, 'network.tx_bytes', 0),
], ],
]; ];
} }

View File

@ -23,6 +23,7 @@
"path": "^0.12.7", "path": "^0.12.7",
"query-string": "^6.7.0", "query-string": "^6.7.0",
"react": "^16.13.1", "react": "^16.13.1",
"react-ga": "^3.1.2",
"react-dom": "npm:@hot-loader/react-dom", "react-dom": "npm:@hot-loader/react-dom",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-google-recaptcha": "^2.0.1", "react-google-recaptcha": "^2.0.1",

View File

@ -2,7 +2,7 @@ import http from '@/api/http';
import { rawDataToFileObject } from '@/api/transformers'; import { rawDataToFileObject } from '@/api/transformers';
export interface FileObject { export interface FileObject {
uuid: string; key: string;
name: string; name: string;
mode: string; mode: string;
size: number; size: number;

View File

@ -1,7 +1,6 @@
import { Allocation } from '@/api/server/getServer'; import { Allocation } from '@/api/server/getServer';
import { FractalResponseData } from '@/api/http'; import { FractalResponseData } from '@/api/http';
import { FileObject } from '@/api/server/files/loadDirectory'; import { FileObject } from '@/api/server/files/loadDirectory';
import v4 from 'uuid/v4';
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({ export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
id: data.attributes.id, id: data.attributes.id,
@ -13,7 +12,7 @@ export const rawDataToServerAllocation = (data: FractalResponseData): Allocation
}); });
export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
uuid: v4(), key: `${data.attributes.is_file ? 'file' : 'dir'}_${data.attributes.name}`,
name: data.attributes.name, name: data.attributes.name,
mode: data.attributes.mode, mode: data.attributes.mode,
size: Number(data.attributes.size), size: Number(data.attributes.size),

View File

@ -6,19 +6,19 @@ export default createGlobalStyle`
${tw`font-sans bg-neutral-800 text-neutral-200`}; ${tw`font-sans bg-neutral-800 text-neutral-200`};
letter-spacing: 0.015em; letter-spacing: 0.015em;
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
${tw`font-medium tracking-normal font-header`}; ${tw`font-medium tracking-normal font-header`};
} }
p { p {
${tw`text-neutral-200 leading-snug font-sans`}; ${tw`text-neutral-200 leading-snug font-sans`};
} }
form { form {
${tw`m-0`}; ${tw`m-0`};
} }
textarea, select, input, button, button:focus, button:focus-visible { textarea, select, input, button, button:focus, button:focus-visible {
${tw`outline-none`}; ${tw`outline-none`};
} }
@ -32,4 +32,41 @@ export default createGlobalStyle`
input[type=number] { input[type=number] {
-moz-appearance: textfield !important; -moz-appearance: textfield !important;
} }
/* Scroll Bar Style */
::-webkit-scrollbar {
background: none;
width: 16px;
height: 16px;
}
::-webkit-scrollbar-thumb {
border: solid 0 rgb(0 0 0 / 0%);
border-right-width: 4px;
border-left-width: 4px;
-webkit-border-radius: 9px 4px;
-webkit-box-shadow: inset 0 0 0 1px hsl(211, 10%, 53%), inset 0 0 0 4px hsl(209deg 18% 30%);
}
::-webkit-scrollbar-track-piece {
margin: 4px 0;
}
::-webkit-scrollbar-thumb:horizontal {
border-right-width: 0;
border-left-width: 0;
border-top-width: 4px;
border-bottom-width: 4px;
-webkit-border-radius: 4px 9px;
}
::-webkit-scrollbar-thumb:hover {
-webkit-box-shadow:
inset 0 0 0 1px hsl(212, 92%, 43%),
inset 0 0 0 4px hsl(212, 92%, 43%);
}
::-webkit-scrollbar-corner {
background: transparent;
}
`; `;

View File

@ -1,4 +1,5 @@
import * as React from 'react'; import React, { useEffect } from 'react';
import ReactGA from 'react-ga';
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { StoreProvider } from 'easy-peasy'; import { StoreProvider } from 'easy-peasy';
@ -48,6 +49,11 @@ const App = () => {
store.getActions().settings.setSettings(SiteConfiguration!); store.getActions().settings.setSettings(SiteConfiguration!);
} }
useEffect(() => {
ReactGA.initialize(SiteConfiguration!.analytics);
ReactGA.pageview(location.pathname);
}, []);
return ( return (
<> <>
<GlobalStylesheet/> <GlobalStylesheet/>

View File

@ -0,0 +1,26 @@
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
import { ServerContext } from '@/state/server';
import useServer from '@/plugins/useServer';
const InstallListener = () => {
const server = useServer();
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
const setServer = ServerContext.useStoreActions(actions => actions.server.setServer);
// Listen for the installation completion event and then fire off a request to fetch the updated
// server information. This allows the server to automatically become available to the user if they
// just sit on the page.
useWebsocketEvent('install completed', () => {
getServer(server.uuid).catch(error => console.error(error));
});
// When we see the install started event immediately update the state to indicate such so that the
// screens automatically update.
useWebsocketEvent('install started', () => {
setServer({ ...server, isInstalling: true });
});
return null;
};
export default InstallListener;

View File

@ -57,7 +57,7 @@ export default () => {
</div> </div>
} }
{featureLimits.backups === 0 && {featureLimits.backups === 0 &&
<p className="text-center text-sm text-neutral-400"> <p css={tw`text-center text-sm text-neutral-400`}>
Backups cannot be created for this server. Backups cannot be created for this server.
</p> </p>
} }

View File

@ -49,7 +49,7 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
</FormikFieldWrapper> </FormikFieldWrapper>
</div> </div>
<div css={tw`flex justify-end`}> <div css={tw`flex justify-end`}>
<Button type={'submit'}> <Button type={'submit'} disabled={isSubmitting}>
Start backup Start backup
</Button> </Button>
</div> </div>
@ -94,11 +94,7 @@ export default () => {
ignored: string(), ignored: string(),
})} })}
> >
<ModalContent <ModalContent appear visible={visible} onDismissed={() => setVisible(false)}/>
appear
visible={visible}
onDismissed={() => setVisible(false)}
/>
</Formik> </Formik>
} }
<Button onClick={() => setVisible(true)}> <Button onClick={() => setVisible(true)}>

View File

@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react'; import React, { memo, useRef, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { import {
faBoxOpen, faBoxOpen,
@ -29,6 +29,7 @@ import styled from 'styled-components/macro';
import useEventListener from '@/plugins/useEventListener'; import useEventListener from '@/plugins/useEventListener';
import compressFiles from '@/api/server/files/compressFiles'; import compressFiles from '@/api/server/files/compressFiles';
import decompressFiles from '@/api/server/files/decompressFiles'; import decompressFiles from '@/api/server/files/decompressFiles';
import isEqual from 'react-fast-compare';
type ModalType = 'rename' | 'move'; type ModalType = 'rename' | 'move';
@ -50,7 +51,7 @@ const Row = ({ icon, title, ...props }: RowProps) => (
</StyledRow> </StyledRow>
); );
export default ({ file }: { file: FileObject }) => { const FileDropdownMenu = ({ file }: { file: FileObject }) => {
const onClickRef = useRef<DropdownMenu>(null); const onClickRef = useRef<DropdownMenu>(null);
const [ showSpinner, setShowSpinner ] = useState(false); const [ showSpinner, setShowSpinner ] = useState(false);
const [ modal, setModal ] = useState<ModalType | null>(null); const [ modal, setModal ] = useState<ModalType | null>(null);
@ -60,7 +61,7 @@ export default ({ file }: { file: FileObject }) => {
const { clearAndAddHttpError, clearFlashes } = useFlash(); const { clearAndAddHttpError, clearFlashes } = useFlash();
const directory = ServerContext.useStoreState(state => state.files.directory); const directory = ServerContext.useStoreState(state => state.files.directory);
useEventListener(`pterodactyl:files:ctx:${file.uuid}`, (e: CustomEvent) => { useEventListener(`pterodactyl:files:ctx:${file.key}`, (e: CustomEvent) => {
if (onClickRef.current) { if (onClickRef.current) {
onClickRef.current.triggerMenu(e.detail); onClickRef.current.triggerMenu(e.detail);
} }
@ -71,7 +72,7 @@ export default ({ file }: { file: FileObject }) => {
// For UI speed, immediately remove the file from the listing before calling the deletion function. // For UI speed, immediately remove the file from the listing before calling the deletion function.
// If the delete actually fails, we'll fetch the current directory contents again automatically. // If the delete actually fails, we'll fetch the current directory contents again automatically.
mutate(files => files.filter(f => f.uuid !== file.uuid), false); mutate(files => files.filter(f => f.key !== file.key), false);
deleteFiles(uuid, directory, [ file.name ]).catch(error => { deleteFiles(uuid, directory, [ file.name ]).catch(error => {
mutate(); mutate();
@ -166,3 +167,5 @@ export default ({ file }: { file: FileObject }) => {
</DropdownMenu> </DropdownMenu>
); );
}; };
export default memo(FileDropdownMenu, isEqual);

View File

@ -71,7 +71,7 @@ export default () => {
} }
{ {
sortFiles(files.slice(0, 250)).map(file => ( sortFiles(files.slice(0, 250)).map(file => (
<FileObjectRow key={file.uuid} file={file}/> <FileObjectRow key={file.key} file={file}/>
)) ))
} }
<MassActionsBar/> <MassActionsBar/>

View File

@ -39,7 +39,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => {
key={file.name} key={file.name}
onContextMenu={e => { onContextMenu={e => {
e.preventDefault(); e.preventDefault();
window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.uuid}`, { detail: e.clientX })); window.dispatchEvent(new CustomEvent(`pterodactyl:files:ctx:${file.key}`, { detail: e.clientX }));
}} }}
> >
<SelectFileCheckbox name={file.name}/> <SelectFileCheckbox name={file.name}/>

View File

@ -6,7 +6,6 @@ import Field from '@/components/elements/Field';
import { join } from 'path'; import { join } from 'path';
import { object, string } from 'yup'; import { object, string } from 'yup';
import createDirectory from '@/api/server/files/createDirectory'; import createDirectory from '@/api/server/files/createDirectory';
import v4 from 'uuid/v4';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import { mutate } from 'swr'; import { mutate } from 'swr';
@ -24,7 +23,7 @@ const schema = object().shape({
}); });
const generateDirectoryData = (name: string): FileObject => ({ const generateDirectoryData = (name: string): FileObject => ({
uuid: v4(), key: `dir_${name}`,
name: name, name: name,
mode: '0644', mode: '0644',
size: 0, size: 0,

View File

@ -9,23 +9,22 @@ import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import useServer from '@/plugins/useServer'; import useServer from '@/plugins/useServer';
import useFileManagerSwr from '@/plugins/useFileManagerSwr'; import useFileManagerSwr from '@/plugins/useFileManagerSwr';
import useFlash from '@/plugins/useFlash'; import withFlash, { WithFlashProps } from '@/hoc/withFlash';
interface FormikValues { interface FormikValues {
name: string; 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 { uuid } = useServer();
const { mutate } = useFileManagerSwr(); const { mutate } = useFileManagerSwr();
const { clearFlashes, clearAndAddHttpError } = useFlash();
const directory = ServerContext.useStoreState(state => state.files.directory); const directory = ServerContext.useStoreState(state => state.files.directory);
const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles); const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
const submit = ({ name }: FormikValues, { setSubmitting }: FormikHelpers<FormikValues>) => { const submit = ({ name }: FormikValues, { setSubmitting }: FormikHelpers<FormikValues>) => {
clearFlashes('files'); flash.clearFlashes('files');
const len = name.split('/').length; const len = name.split('/').length;
if (files.length === 1) { if (files.length === 1) {
@ -51,7 +50,7 @@ export default ({ files, useMoveTerminology, ...props }: Props) => {
.catch(error => { .catch(error => {
mutate(); mutate();
setSubmitting(false); setSubmitting(false);
clearAndAddHttpError({ key: 'files', error }); flash.clearAndAddHttpError({ key: 'files', error });
}) })
.then(() => props.onDismissed()); .then(() => props.onDismissed());
}; };
@ -96,3 +95,5 @@ export default ({ files, useMoveTerminology, ...props }: Props) => {
</Formik> </Formik>
); );
}; };
export default withFlash(RenameFileModal);

View File

@ -65,7 +65,7 @@ const EditScheduleModal = ({ schedule, ...props }: Omit<Props, 'onScheduleUpdate
/> />
</div> </div>
<div css={tw`mt-6 text-right`}> <div css={tw`mt-6 text-right`}>
<Button type={'submit'}> <Button type={'submit'} disabled={isSubmitting}>
{schedule ? 'Save changes' : 'Create schedule'} {schedule ? 'Save changes' : 'Create schedule'}
</Button> </Button>
</div> </div>

View File

@ -14,7 +14,7 @@ export default ({ schedule }: { schedule: Schedule }) => (
<p>{schedule.name}</p> <p>{schedule.name}</p>
<p css={tw`text-xs text-neutral-400`}> <p css={tw`text-xs text-neutral-400`}>
Last run Last run
at: {schedule.lastRunAt ? format(schedule.lastRunAt, 'MMM Do [at] h:mma') : 'never'} at: {schedule.lastRunAt ? format(schedule.lastRunAt, 'MMM do \'at\' h:mma') : 'never'}
</p> </p>
</div> </div>
<div css={tw`flex items-center mx-8`}> <div css={tw`flex items-center mx-8`}>

View File

@ -32,11 +32,16 @@ interface Values {
} }
const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => { const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
const { values: { action }, setFieldValue, setFieldTouched } = useFormikContext<Values>(); const { values: { action }, initialValues, setFieldValue, setFieldTouched, isSubmitting } = useFormikContext<Values>();
useEffect(() => { useEffect(() => {
setFieldValue('payload', action === 'power' ? 'start' : ''); if (action !== initialValues.action) {
setFieldTouched('payload', false); setFieldValue('payload', action === 'power' ? 'start' : '');
setFieldTouched('payload', false);
} else {
setFieldValue('payload', initialValues.payload);
setFieldTouched('payload', false);
}
}, [ action ]); }, [ action ]);
return ( return (
@ -94,7 +99,7 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
/> />
</div> </div>
<div css={tw`flex justify-end mt-6`}> <div css={tw`flex justify-end mt-6`}>
<Button type={'submit'}> <Button type={'submit'} disabled={isSubmitting}>
{isEditingTask ? 'Save Changes' : 'Create Task'} {isEditingTask ? 'Save Changes' : 'Create Task'}
</Button> </Button>
</div> </div>

View File

@ -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<ApplicationStore>['flashes'];
}
function withFlash<TOwnProps> (Component: React.ComponentType<TOwnProps & WithFlashProps>): React.ComponentType<TOwnProps> {
return (props: TOwnProps) => {
const { addError, addFlash, clearFlashes, clearAndAddHttpError } = useFlash();
return (
<Component
flash={{ addError, addFlash, clearFlashes, clearAndAddHttpError }}
{...props}
/>
);
};
}
export default withFlash;

View File

@ -1,9 +1,8 @@
import { DependencyList } from 'react';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { Server } from '@/api/server/getServer'; import { Server } from '@/api/server/getServer';
const useServer = (dependencies?: DependencyList): Server => { const useServer = (dependencies?: any[] | undefined): Server => {
return ServerContext.useStoreState(state => state.server.data!, [ dependencies ]); return ServerContext.useStoreState(state => state.server.data!, dependencies);
}; };
export default useServer; export default useServer;

View File

@ -1,4 +1,5 @@
import React from 'react'; import React, { useEffect } from 'react';
import ReactGA from 'react-ga';
import { Route, RouteComponentProps, Switch } from 'react-router-dom'; import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import LoginContainer from '@/components/auth/LoginContainer'; import LoginContainer from '@/components/auth/LoginContainer';
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer'; import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
@ -6,17 +7,23 @@ import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer'; import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
import NotFound from '@/components/screens/NotFound'; import NotFound from '@/components/screens/NotFound';
export default ({ location, history, match }: RouteComponentProps) => ( export default ({ location, history, match }: RouteComponentProps) => {
<div className={'pt-8 xl:pt-32'}> useEffect(() => {
<Switch location={location}> ReactGA.pageview(location.pathname);
<Route path={`${match.path}/login`} component={LoginContainer} exact/> }, [ location.pathname ]);
<Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
<Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/> return (
<Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/> <div className={'pt-8 xl:pt-32'}>
<Route path={`${match.path}/checkpoint`}/> <Switch location={location}>
<Route path={'*'}> <Route path={`${match.path}/login`} component={LoginContainer} exact/>
<NotFound onBack={() => history.push('/auth/login')}/> <Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
</Route> <Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
</Switch> <Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
</div> <Route path={`${match.path}/checkpoint`} />
); <Route path={'*'}>
<NotFound onBack={() => history.push('/auth/login')} />
</Route>
</Switch>
</div>
);
};

View File

@ -1,4 +1,5 @@
import * as React from 'react'; import React, { useEffect } from 'react';
import ReactGA from 'react-ga';
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer'; import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer';
import NavigationBar from '@/components/NavigationBar'; import NavigationBar from '@/components/NavigationBar';
@ -8,24 +9,30 @@ import NotFound from '@/components/screens/NotFound';
import TransitionRouter from '@/TransitionRouter'; import TransitionRouter from '@/TransitionRouter';
import SubNavigation from '@/components/elements/SubNavigation'; import SubNavigation from '@/components/elements/SubNavigation';
export default ({ location }: RouteComponentProps) => ( export default ({ location }: RouteComponentProps) => {
<> useEffect(() => {
<NavigationBar/> ReactGA.pageview(location.pathname);
{location.pathname.startsWith('/account') && }, [ location.pathname ]);
<SubNavigation>
<div> return (
<NavLink to={'/account'} exact>Settings</NavLink> <>
<NavLink to={'/account/api'}>API Credentials</NavLink> <NavigationBar />
</div> {location.pathname.startsWith('/account') &&
</SubNavigation> <SubNavigation>
} <div>
<TransitionRouter> <NavLink to={'/account'} exact>Settings</NavLink>
<Switch location={location}> <NavLink to={'/account/api'}>API Credentials</NavLink>
<Route path={'/'} component={DashboardContainer} exact/> </div>
<Route path={'/account'} component={AccountOverviewContainer} exact/> </SubNavigation>
<Route path={'/account/api'} component={AccountApiContainer} exact/> }
<Route path={'*'} component={NotFound}/> <TransitionRouter>
</Switch> <Switch location={location}>
</TransitionRouter> <Route path={'/'} component={DashboardContainer} exact />
</> <Route path={'/account'} component={AccountOverviewContainer} exact/>
); <Route path={'/account/api'} component={AccountApiContainer} exact/>
<Route path={'*'} component={NotFound} />
</Switch>
</TransitionRouter>
</>
);
};

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import ReactGA from 'react-ga';
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
import NavigationBar from '@/components/NavigationBar'; import NavigationBar from '@/components/NavigationBar';
import ServerConsole from '@/components/server/ServerConsole'; import ServerConsole from '@/components/server/ServerConsole';
@ -25,6 +26,7 @@ import useServer from '@/plugins/useServer';
import ScreenBlock from '@/components/screens/ScreenBlock'; import ScreenBlock from '@/components/screens/ScreenBlock';
import SubNavigation from '@/components/elements/SubNavigation'; import SubNavigation from '@/components/elements/SubNavigation';
import NetworkContainer from '@/components/server/network/NetworkContainer'; import NetworkContainer from '@/components/server/network/NetworkContainer';
import InstallListener from '@/components/server/InstallListener';
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
const { rootAdmin } = useStoreState(state => state.user.data!); const { rootAdmin } = useStoreState(state => state.user.data!);
@ -60,6 +62,10 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
}; };
}, [ match.params.id ]); }, [ match.params.id ]);
useEffect(() => {
ReactGA.pageview(location.pathname);
}, [ location.pathname ]);
return ( return (
<React.Fragment key={'server-router'}> <React.Fragment key={'server-router'}>
<NavigationBar/> <NavigationBar/>
@ -98,6 +104,8 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
</div> </div>
</SubNavigation> </SubNavigation>
</CSSTransition> </CSSTransition>
<InstallListener/>
<WebsocketHandler/>
{(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ? {(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ?
<ScreenBlock <ScreenBlock
title={'Your server is installing.'} title={'Your server is installing.'}
@ -106,7 +114,6 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
/> />
: :
<> <>
<WebsocketHandler/>
<TransitionRouter> <TransitionRouter>
<Switch location={location}> <Switch location={location}>
<Route path={`${match.path}`} component={ServerConsole} exact/> <Route path={`${match.path}`} component={ServerConsole} exact/>

View File

@ -7,6 +7,7 @@ export interface SiteSettings {
enabled: boolean; enabled: boolean;
siteKey: string; siteKey: string;
}; };
analytics: string;
} }
export interface SettingsStore { export interface SettingsStore {

View File

@ -31,6 +31,13 @@
<p class="text-muted"><small>This is the name that is used throughout the panel and in emails sent to clients.</small></p> <p class="text-muted"><small>This is the name that is used throughout the panel and in emails sent to clients.</small></p>
</div> </div>
</div> </div>
<div class="form-group col-md-4">
<label class="control-label">Google Analytics</label>
<div>
<input type="text" class="form-control" name="app:analytics" value="{{ old('app:analytics', config('app.analytics')) }}" />
<p class="text-muted"><small>This is your Google Analytics Tracking ID, Ex. UA-123723645-2</small></p>
</div>
</div>
<div class="form-group col-md-4"> <div class="form-group col-md-4">
<label class="control-label">Require 2-Factor Authentication</label> <label class="control-label">Require 2-Factor Authentication</label>
<div> <div>

View File

@ -5569,6 +5569,11 @@ 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" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-ga@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.1.2.tgz#e13f211c51a2e5c401ea69cf094b9501fe3c51ce"
integrity sha512-OJrMqaHEHbodm+XsnjA6ISBEHTwvpFrxco65mctzl/v3CASMSLSyUkFqz9yYrPDKGBUfNQzKCjuMJwctjlWBbw==
react-google-recaptcha@^2.0.1: react-google-recaptcha@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8" resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz#3276b29659493f7ca2a5b7739f6c239293cdf1d8"