React 18 and Vite (#4510)
This commit is contained in:
parent
1bb1b13f6d
commit
21613fa602
244 changed files with 4547 additions and 8933 deletions
|
@ -1,23 +1,20 @@
|
|||
import React, { lazy } from 'react';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
import { StoreProvider } from 'easy-peasy';
|
||||
import { store } from '@/state';
|
||||
import { SiteSettings } from '@/state/settings';
|
||||
import { lazy } from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import '@/assets/tailwind.css';
|
||||
import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
|
||||
import AuthenticatedRoute from '@/components/elements/AuthenticatedRoute';
|
||||
import ProgressBar from '@/components/elements/ProgressBar';
|
||||
import { NotFound } from '@/components/elements/ScreenBlock';
|
||||
import tw from 'twin.macro';
|
||||
import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
|
||||
import { history } from '@/components/history';
|
||||
import { setupInterceptors } from '@/api/interceptors';
|
||||
import AuthenticatedRoute from '@/components/elements/AuthenticatedRoute';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import '@/assets/tailwind.css';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { store } from '@/state';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { SiteSettings } from '@/state/settings';
|
||||
|
||||
const DashboardRouter = lazy(() => import(/* webpackChunkName: "dashboard" */ '@/routers/DashboardRouter'));
|
||||
const ServerRouter = lazy(() => import(/* webpackChunkName: "server" */ '@/routers/ServerRouter'));
|
||||
const AuthenticationRouter = lazy(() => import(/* webpackChunkName: "auth" */ '@/routers/AuthenticationRouter'));
|
||||
const DashboardRouter = lazy(() => import('@/routers/DashboardRouter'));
|
||||
const ServerRouter = lazy(() => import('@/routers/ServerRouter'));
|
||||
const AuthenticationRouter = lazy(() => import('@/routers/AuthenticationRouter'));
|
||||
|
||||
interface ExtendedWindow extends Window {
|
||||
SiteConfiguration?: SiteSettings;
|
||||
|
@ -35,9 +32,9 @@ interface ExtendedWindow extends Window {
|
|||
};
|
||||
}
|
||||
|
||||
setupInterceptors(history);
|
||||
// setupInterceptors(history);
|
||||
|
||||
const App = () => {
|
||||
function App() {
|
||||
const { PterodactylUser, SiteConfiguration } = window as ExtendedWindow;
|
||||
if (PterodactylUser && !store.getState().user.data) {
|
||||
store.getActions().user.setUserData({
|
||||
|
@ -58,38 +55,55 @@ const App = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{/* @ts-expect-error go away */}
|
||||
<GlobalStylesheet />
|
||||
|
||||
<StoreProvider store={store}>
|
||||
<ProgressBar />
|
||||
<div css={tw`mx-auto w-auto`}>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={'/auth'}>
|
||||
<Spinner.Suspense>
|
||||
<AuthenticationRouter />
|
||||
</Spinner.Suspense>
|
||||
</Route>
|
||||
<AuthenticatedRoute path={'/server/:id'}>
|
||||
<Spinner.Suspense>
|
||||
<ServerContext.Provider>
|
||||
<ServerRouter />
|
||||
</ServerContext.Provider>
|
||||
</Spinner.Suspense>
|
||||
</AuthenticatedRoute>
|
||||
<AuthenticatedRoute path={'/'}>
|
||||
<Spinner.Suspense>
|
||||
<DashboardRouter />
|
||||
</Spinner.Suspense>
|
||||
</AuthenticatedRoute>
|
||||
<Route path={'*'}>
|
||||
<NotFound />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
<div className="mx-auto w-auto">
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/auth/*"
|
||||
element={
|
||||
<Spinner.Suspense>
|
||||
<AuthenticationRouter />
|
||||
</Spinner.Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/server/:id/*"
|
||||
element={
|
||||
<AuthenticatedRoute>
|
||||
<Spinner.Suspense>
|
||||
<ServerContext.Provider>
|
||||
<ServerRouter />
|
||||
</ServerContext.Provider>
|
||||
</Spinner.Suspense>
|
||||
</AuthenticatedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<AuthenticatedRoute>
|
||||
<Spinner.Suspense>
|
||||
<DashboardRouter />
|
||||
</Spinner.Suspense>
|
||||
</AuthenticatedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
</StoreProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default hot(App);
|
||||
export { App };
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import BoringAvatar, { AvatarProps } from 'boring-avatars';
|
||||
import { useStoreState } from '@/state/hooks';
|
||||
|
||||
|
@ -11,7 +10,7 @@ const _Avatar = ({ variant = 'beam', ...props }: AvatarProps) => (
|
|||
);
|
||||
|
||||
const _UserAvatar = ({ variant = 'beam', ...props }: Omit<Props, 'name'>) => {
|
||||
const uuid = useStoreState((state) => state.user.data?.uuid);
|
||||
const uuid = useStoreState(state => state.user.data?.uuid);
|
||||
|
||||
return <BoringAvatar colors={palette} name={uuid || 'system'} variant={variant} {...props} />;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import MessageBox from '@/components/MessageBox';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import tw from 'twin.macro';
|
||||
|
@ -9,19 +9,17 @@ type Props = Readonly<{
|
|||
}>;
|
||||
|
||||
const FlashMessageRender = ({ byKey, className }: Props) => {
|
||||
const flashes = useStoreState((state) =>
|
||||
state.flashes.items.filter((flash) => (byKey ? flash.key === byKey : true))
|
||||
);
|
||||
const flashes = useStoreState(state => state.flashes.items.filter(flash => (byKey ? flash.key === byKey : true)));
|
||||
|
||||
return flashes.length ? (
|
||||
<div className={className}>
|
||||
{flashes.map((flash, index) => (
|
||||
<React.Fragment key={flash.id || flash.type + flash.message}>
|
||||
<Fragment key={flash.id || flash.type + flash.message}>
|
||||
{index > 0 && <div css={tw`mt-2`}></div>}
|
||||
<MessageBox type={flash.type} title={flash.title}>
|
||||
{flash.message}
|
||||
</MessageBox>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import tw, { TwStyle } from 'twin.macro';
|
||||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export type FlashMessageType = 'success' | 'info' | 'warning' | 'error';
|
||||
|
||||
|
@ -42,7 +41,7 @@ const getBackground = (type?: FlashMessageType): TwStyle | string => {
|
|||
|
||||
const Container = styled.div<{ $type?: FlashMessageType }>`
|
||||
${tw`p-2 border items-center leading-normal rounded flex w-full text-sm text-white`};
|
||||
${(props) => styling(props.$type)};
|
||||
${props => styling(props.$type)};
|
||||
`;
|
||||
Container.displayName = 'MessageBox.Container';
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
@ -7,7 +6,7 @@ import { useStoreState } from 'easy-peasy';
|
|||
import { ApplicationStore } from '@/state';
|
||||
import SearchContainer from '@/components/dashboard/search/SearchContainer';
|
||||
import tw, { theme } from 'twin.macro';
|
||||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import http from '@/api/http';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import Tooltip from '@/components/elements/tooltip/Tooltip';
|
||||
|
@ -39,6 +38,7 @@ export default () => {
|
|||
|
||||
const onTriggerLogout = () => {
|
||||
setIsLoggingOut(true);
|
||||
|
||||
http.post('/auth/logout').finally(() => {
|
||||
// @ts-expect-error this is valid
|
||||
window.location = '/';
|
||||
|
@ -46,41 +46,43 @@ export default () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={'w-full bg-neutral-900 shadow-md overflow-x-auto'}>
|
||||
<div className="w-full bg-neutral-900 shadow-md overflow-x-auto">
|
||||
<SpinnerOverlay visible={isLoggingOut} />
|
||||
<div className={'mx-auto w-full flex items-center h-[3.5rem] max-w-[1200px]'}>
|
||||
<div id={'logo'} className={'flex-1'}>
|
||||
<div className="mx-auto w-full flex items-center h-[3.5rem] max-w-[1200px]">
|
||||
<div id="logo" className="flex-1">
|
||||
<Link
|
||||
to={'/'}
|
||||
className={
|
||||
'text-2xl font-header px-4 no-underline text-neutral-200 hover:text-neutral-100 transition-colors duration-150'
|
||||
}
|
||||
to="/"
|
||||
className="text-2xl font-header px-4 no-underline text-neutral-200 hover:text-neutral-100 transition-colors duration-150"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</div>
|
||||
<RightNavigation className={'flex h-full items-center justify-center'}>
|
||||
<RightNavigation className="flex h-full items-center justify-center">
|
||||
<SearchContainer />
|
||||
<Tooltip placement={'bottom'} content={'Dashboard'}>
|
||||
<NavLink to={'/'} exact>
|
||||
|
||||
<Tooltip placement="bottom" content="Dashboard">
|
||||
<NavLink to="/" end>
|
||||
<FontAwesomeIcon icon={faLayerGroup} />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
|
||||
{rootAdmin && (
|
||||
<Tooltip placement={'bottom'} content={'Admin'}>
|
||||
<a href={'/admin'} rel={'noreferrer'}>
|
||||
<Tooltip placement="bottom" content="Admin">
|
||||
<a href="/admin" rel="noreferrer">
|
||||
<FontAwesomeIcon icon={faCogs} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip placement={'bottom'} content={'Account Settings'}>
|
||||
<NavLink to={'/account'}>
|
||||
<span className={'flex items-center w-5 h-5'}>
|
||||
|
||||
<Tooltip placement="bottom" content="Account Settings">
|
||||
<NavLink to="/account">
|
||||
<span className="flex items-center w-5 h-5">
|
||||
<Avatar.User />
|
||||
</span>
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
<Tooltip placement={'bottom'} content={'Sign Out'}>
|
||||
|
||||
<Tooltip placement="bottom" content="Sign Out">
|
||||
<button onClick={onTriggerLogout}>
|
||||
<FontAwesomeIcon icon={faSignOutAlt} />
|
||||
</button>
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
import * as React from 'react';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import type { FormikHelpers } from 'formik';
|
||||
import { Formik } from 'formik';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Reaptcha from 'reaptcha';
|
||||
import tw from 'twin.macro';
|
||||
import { object, string } from 'yup';
|
||||
|
||||
import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
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 Field from '@/components/elements/Field';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
||||
interface Values {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export default () => {
|
||||
function ForgotPasswordContainer() {
|
||||
const ref = useRef<Reaptcha>(null);
|
||||
const [token, setToken] = useState('');
|
||||
|
||||
const { clearFlashes, addFlash } = useFlash();
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha);
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes();
|
||||
|
@ -34,7 +35,7 @@ export default () => {
|
|||
// 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) => {
|
||||
ref.current!.execute().catch(error => {
|
||||
console.error(error);
|
||||
|
||||
setSubmitting(false);
|
||||
|
@ -45,17 +46,19 @@ export default () => {
|
|||
}
|
||||
|
||||
requestPasswordResetEmail(email, token)
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
resetForm();
|
||||
addFlash({ type: 'success', title: 'Success', message: response });
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
||||
})
|
||||
.then(() => {
|
||||
setToken('');
|
||||
if (ref.current) ref.current.reset();
|
||||
if (ref.current !== null) {
|
||||
void ref.current.reset();
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
});
|
||||
|
@ -92,9 +95,9 @@ export default () => {
|
|||
ref={ref}
|
||||
size={'invisible'}
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onVerify={(response) => {
|
||||
onVerify={response => {
|
||||
setToken(response);
|
||||
submitForm();
|
||||
void submitForm();
|
||||
}}
|
||||
onExpire={() => {
|
||||
setSubmitting(false);
|
||||
|
@ -114,4 +117,6 @@ export default () => {
|
|||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ForgotPasswordContainer;
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import type { ActionCreator } from 'easy-peasy';
|
||||
import { useFormikContext, withFormik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import type { Location, RouteProps } from 'react-router-dom';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
import loginCheckpoint from '@/api/auth/loginCheckpoint';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { ActionCreator } from 'easy-peasy';
|
||||
import { StaticContext } from 'react-router';
|
||||
import { useFormikContext, withFormik } from 'formik';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { FlashStore } from '@/state/flashes';
|
||||
import Field from '@/components/elements/Field';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Field from '@/components/elements/Field';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import type { FlashStore } from '@/state/flashes';
|
||||
|
||||
interface Values {
|
||||
code: string;
|
||||
recoveryCode: '';
|
||||
}
|
||||
|
||||
type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }>;
|
||||
type OwnProps = RouteProps;
|
||||
|
||||
type Props = OwnProps & {
|
||||
clearAndAddHttpError: ActionCreator<FlashStore['clearAndAddHttpError']['payload']>;
|
||||
};
|
||||
|
||||
const LoginCheckpointContainer = () => {
|
||||
function LoginCheckpointContainer() {
|
||||
const { isSubmitting, setFieldValue } = useFormikContext<Values>();
|
||||
const [isMissingDevice, setIsMissingDevice] = useState(false);
|
||||
|
||||
|
@ -53,7 +54,7 @@ const LoginCheckpointContainer = () => {
|
|||
onClick={() => {
|
||||
setFieldValue('code', '');
|
||||
setFieldValue('recoveryCode', '');
|
||||
setIsMissingDevice((s) => !s);
|
||||
setIsMissingDevice(s => !s);
|
||||
}}
|
||||
css={tw`cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
|
@ -70,12 +71,12 @@ const LoginCheckpointContainer = () => {
|
|||
</div>
|
||||
</LoginFormContainer>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const EnhancedForm = withFormik<Props, Values>({
|
||||
const EnhancedForm = withFormik<Props & { location: Location }, Values>({
|
||||
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { clearAndAddHttpError, location } }) => {
|
||||
loginCheckpoint(location.state?.token || '', code, recoveryCode)
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
if (response.complete) {
|
||||
// @ts-expect-error this is valid
|
||||
window.location = response.intended || '/';
|
||||
|
@ -84,7 +85,7 @@ const EnhancedForm = withFormik<Props, Values>({
|
|||
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
setSubmitting(false);
|
||||
clearAndAddHttpError({ error });
|
||||
|
@ -97,16 +98,17 @@ const EnhancedForm = withFormik<Props, Values>({
|
|||
}),
|
||||
})(LoginCheckpointContainer);
|
||||
|
||||
export default ({ history, location, ...props }: OwnProps) => {
|
||||
export default ({ ...props }: OwnProps) => {
|
||||
const { clearAndAddHttpError } = useFlash();
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!location.state?.token) {
|
||||
history.replace('/auth/login');
|
||||
navigate('/auth/login');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EnhancedForm clearAndAddHttpError={clearAndAddHttpError} history={history} location={location} {...props} />
|
||||
);
|
||||
return <EnhancedForm clearAndAddHttpError={clearAndAddHttpError} location={location} {...props} />;
|
||||
};
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import type { FormikHelpers } from 'formik';
|
||||
import { Formik } from 'formik';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import Reaptcha from 'reaptcha';
|
||||
import tw from 'twin.macro';
|
||||
import { object, string } from 'yup';
|
||||
|
||||
import login from '@/api/auth/login';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
import Field from '@/components/elements/Field';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Reaptcha from 'reaptcha';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
||||
interface Values {
|
||||
|
@ -16,12 +18,14 @@ interface Values {
|
|||
password: string;
|
||||
}
|
||||
|
||||
const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||
function LoginContainer() {
|
||||
const ref = useRef<Reaptcha>(null);
|
||||
const [token, setToken] = useState('');
|
||||
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha);
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes();
|
||||
|
@ -33,7 +37,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||
// 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) => {
|
||||
ref.current!.execute().catch(error => {
|
||||
console.error(error);
|
||||
|
||||
setSubmitting(false);
|
||||
|
@ -44,16 +48,16 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||
}
|
||||
|
||||
login({ ...values, recaptchaData: token })
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
if (response.complete) {
|
||||
// @ts-expect-error this is valid
|
||||
window.location = response.intended || '/';
|
||||
return;
|
||||
}
|
||||
|
||||
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
||||
navigate('/auth/login/checkpoint', { state: { token: response.confirmationToken } });
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
setToken('');
|
||||
|
@ -89,7 +93,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||
ref={ref}
|
||||
size={'invisible'}
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onVerify={(response) => {
|
||||
onVerify={response => {
|
||||
setToken(response);
|
||||
submitForm();
|
||||
}}
|
||||
|
@ -111,6 +115,6 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
|||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default LoginContainer;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Form } from 'formik';
|
||||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import { breakpoint } from '@/theme';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import tw from 'twin.macro';
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import performPasswordReset from '@/api/auth/performPasswordReset';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
|
@ -18,7 +17,7 @@ interface Values {
|
|||
passwordConfirmation: string;
|
||||
}
|
||||
|
||||
export default ({ match, location }: RouteComponentProps<{ token: string }>) => {
|
||||
function ResetPasswordContainer() {
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
@ -28,14 +27,16 @@ export default ({ match, location }: RouteComponentProps<{ token: string }>) =>
|
|||
setEmail(parsed.get('email') || '');
|
||||
}
|
||||
|
||||
const params = useParams<'token'>();
|
||||
|
||||
const submit = ({ password, passwordConfirmation }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes();
|
||||
performPasswordReset(email, { token: match.params.token, password, passwordConfirmation })
|
||||
performPasswordReset(email, { token: params.token ?? '', password, passwordConfirmation })
|
||||
.then(() => {
|
||||
// @ts-expect-error this is valid
|
||||
window.location = '/';
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
setSubmitting(false);
|
||||
|
@ -56,7 +57,6 @@ export default ({ match, location }: RouteComponentProps<{ token: string }>) =>
|
|||
.min(8, 'Your new password should be at least 8 characters in length.'),
|
||||
passwordConfirmation: string()
|
||||
.required('Your new password does not match.')
|
||||
// @ts-expect-error this is valid
|
||||
.oneOf([ref('password'), null], 'Your new password does not match.'),
|
||||
})}
|
||||
>
|
||||
|
@ -95,4 +95,6 @@ export default ({ match, location }: RouteComponentProps<{ token: string }>) =>
|
|||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ResetPasswordContainer;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ContentBox from '@/components/elements/ContentBox';
|
||||
import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm';
|
||||
import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
|
||||
|
@ -23,9 +23,9 @@ export default () => {
|
|||
|
||||
useEffect(() => {
|
||||
getApiKeys()
|
||||
.then((keys) => setKeys(keys))
|
||||
.then(keys => setKeys(keys))
|
||||
.then(() => setLoading(false))
|
||||
.catch((error) => clearAndAddHttpError(error));
|
||||
.catch(error => clearAndAddHttpError(error));
|
||||
}, []);
|
||||
|
||||
const doDeletion = (identifier: string) => {
|
||||
|
@ -33,8 +33,8 @@ export default () => {
|
|||
|
||||
clearAndAddHttpError();
|
||||
deleteApiKey(identifier)
|
||||
.then(() => setKeys((s) => [...(s || []).filter((key) => key.identifier !== identifier)]))
|
||||
.catch((error) => clearAndAddHttpError(error))
|
||||
.then(() => setKeys(s => [...(s || []).filter(key => key.identifier !== identifier)]))
|
||||
.catch(error => clearAndAddHttpError(error))
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
setDeleteIdentifier('');
|
||||
|
@ -46,7 +46,7 @@ export default () => {
|
|||
<FlashMessageRender byKey={'account'} />
|
||||
<div css={tw`md:flex flex-nowrap my-10`}>
|
||||
<ContentBox title={'Create API Key'} css={tw`flex-none w-full md:w-1/2`}>
|
||||
<CreateApiKeyForm onKeyCreated={(key) => setKeys((s) => [...s!, key])} />
|
||||
<CreateApiKeyForm onKeyCreated={key => setKeys(s => [...s!, key])} />
|
||||
</ContentBox>
|
||||
<ContentBox title={'API Keys'} css={tw`flex-1 overflow-hidden mt-8 md:mt-0 md:ml-8`}>
|
||||
<SpinnerOverlay visible={loading} />
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import * as React from 'react';
|
||||
import ContentBox from '@/components/elements/ContentBox';
|
||||
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
|
||||
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
|
||||
|
@ -6,7 +5,7 @@ import ConfigureTwoFactorForm from '@/components/dashboard/forms/ConfigureTwoFac
|
|||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import tw from 'twin.macro';
|
||||
import { breakpoint } from '@/theme';
|
||||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import MessageBox from '@/components/MessageBox';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
|
@ -27,24 +26,26 @@ const Container = styled.div`
|
|||
`;
|
||||
|
||||
export default () => {
|
||||
const { state } = useLocation<undefined | { twoFactorRedirect?: boolean }>();
|
||||
const { state } = useLocation();
|
||||
|
||||
return (
|
||||
<PageContentBlock title={'Account Overview'}>
|
||||
<PageContentBlock title="Account Overview">
|
||||
{state?.twoFactorRedirect && (
|
||||
<MessageBox title={'2-Factor Required'} type={'error'}>
|
||||
<MessageBox title="2-Factor Required" type="error">
|
||||
Your account must have two-factor authentication enabled in order to continue.
|
||||
</MessageBox>
|
||||
)}
|
||||
|
||||
<Container css={[tw`lg:grid lg:grid-cols-3 mb-10`, state?.twoFactorRedirect ? tw`mt-4` : tw`mt-10`]}>
|
||||
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
|
||||
<ContentBox title="Update Password" showFlashes="account:password">
|
||||
<UpdatePasswordForm />
|
||||
</ContentBox>
|
||||
<ContentBox css={tw`mt-8 sm:mt-0 sm:ml-8`} title={'Update Email Address'} showFlashes={'account:email'}>
|
||||
|
||||
<ContentBox css={tw`mt-8 sm:mt-0 sm:ml-8`} title="Update Email Address" showFlashes="account:email">
|
||||
<UpdateEmailAddressForm />
|
||||
</ContentBox>
|
||||
<ContentBox css={tw`md:ml-8 mt-8 md:mt-0`} title={'Two-Step Verification'}>
|
||||
|
||||
<ContentBox css={tw`md:ml-8 mt-8 md:mt-0`} title="Two-Step Verification">
|
||||
<ConfigureTwoFactorForm />
|
||||
</ContentBox>
|
||||
</Container>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import asModal from '@/hoc/asModal';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Server } from '@/api/server/getServer';
|
||||
import getServers from '@/api/getServers';
|
||||
import ServerRow from '@/components/dashboard/ServerRow';
|
||||
|
@ -20,13 +20,13 @@ export default () => {
|
|||
|
||||
const [page, setPage] = useState(!isNaN(defaultPage) && defaultPage > 0 ? defaultPage : 1);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const uuid = useStoreState((state) => state.user.data!.uuid);
|
||||
const rootAdmin = useStoreState((state) => state.user.data!.rootAdmin);
|
||||
const uuid = useStoreState(state => state.user.data!.uuid);
|
||||
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
||||
const [showOnlyAdmin, setShowOnlyAdmin] = usePersistedState(`${uuid}:show_all_servers`, false);
|
||||
|
||||
const { data: servers, error } = useSWR<PaginatedResult<Server>>(
|
||||
['/api/client/servers', showOnlyAdmin && rootAdmin, page],
|
||||
() => getServers({ page, type: showOnlyAdmin && rootAdmin ? 'admin' : undefined })
|
||||
() => getServers({ page, type: showOnlyAdmin && rootAdmin ? 'admin' : undefined }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -58,7 +58,7 @@ export default () => {
|
|||
<Switch
|
||||
name={'show_all_servers'}
|
||||
defaultChecked={showOnlyAdmin}
|
||||
onChange={() => setShowOnlyAdmin((s) => !s)}
|
||||
onChange={() => setShowOnlyAdmin(s => !s)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { memo, useEffect, useRef, useState } from 'react';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -8,7 +9,7 @@ import { bytesToString, ip, mbToBytes } from '@/lib/formatters';
|
|||
import tw from 'twin.macro';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
// Determines if the current value is in an alarm threshold so we can show it in red rather
|
||||
|
@ -17,14 +18,14 @@ const isAlarmState = (current: number, limit: number): boolean => limit > 0 && c
|
|||
|
||||
const Icon = memo(
|
||||
styled(FontAwesomeIcon)<{ $alarm: boolean }>`
|
||||
${(props) => (props.$alarm ? tw`text-red-400` : tw`text-neutral-500`)};
|
||||
${props => (props.$alarm ? tw`text-red-400` : tw`text-neutral-500`)};
|
||||
`,
|
||||
isEqual
|
||||
isEqual,
|
||||
);
|
||||
|
||||
const IconDescription = styled.p<{ $alarm: boolean }>`
|
||||
${tw`text-sm ml-2`};
|
||||
${(props) => (props.$alarm ? tw`text-white` : tw`text-neutral-400`)};
|
||||
${props => (props.$alarm ? tw`text-white` : tw`text-neutral-400`)};
|
||||
`;
|
||||
|
||||
const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>`
|
||||
|
@ -56,8 +57,8 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
|
||||
const getStats = () =>
|
||||
getServerResourceUsage(server.uuid)
|
||||
.then((data) => setStats(data))
|
||||
.catch((error) => console.error(error));
|
||||
.then(data => setStats(data))
|
||||
.catch(error => console.error(error));
|
||||
|
||||
useEffect(() => {
|
||||
setIsSuspended(stats?.isSuspended || server.status === 'suspended');
|
||||
|
@ -106,8 +107,8 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`} />
|
||||
<p css={tw`text-sm text-neutral-400 ml-2`}>
|
||||
{server.allocations
|
||||
.filter((alloc) => alloc.isDefault)
|
||||
.map((allocation) => (
|
||||
.filter(alloc => alloc.isDefault)
|
||||
.map(allocation => (
|
||||
<React.Fragment key={allocation.ip + allocation.port.toString()}>
|
||||
{allocation.alias || ip(allocation.ip)}:{allocation.port}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ActivityLogFilters, useActivityLogs } from '@/api/account/activity';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
|
@ -23,7 +23,7 @@ export default () => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
setFilters((value) => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
|
||||
setFilters(value => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
|
||||
}, [hash]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -38,7 +38,7 @@ export default () => {
|
|||
<Link
|
||||
to={'#'}
|
||||
className={classNames(btnStyles.button, btnStyles.text, 'w-full sm:w-auto')}
|
||||
onClick={() => setFilters((value) => ({ ...value, filters: {} }))}
|
||||
onClick={() => setFilters(value => ({ ...value, filters: {} }))}
|
||||
>
|
||||
Clear Filters <XCircleIcon className={'w-4 h-4 ml-2'} />
|
||||
</Link>
|
||||
|
@ -48,7 +48,7 @@ export default () => {
|
|||
<Spinner centered />
|
||||
) : (
|
||||
<div className={'bg-gray-700'}>
|
||||
{data?.items.map((activity) => (
|
||||
{data?.items.map(activity => (
|
||||
<ActivityLogEntry key={activity.id} activity={activity}>
|
||||
{typeof activity.properties.useragent === 'string' && (
|
||||
<Tooltip content={activity.properties.useragent} placement={'top'}>
|
||||
|
@ -64,7 +64,7 @@ export default () => {
|
|||
{data && (
|
||||
<PaginationFooter
|
||||
pagination={data.pagination}
|
||||
onPageSelect={(page) => setFilters((value) => ({ ...value, page }))}
|
||||
onPageSelect={page => setFilters(value => ({ ...value, page }))}
|
||||
/>
|
||||
)}
|
||||
</PageContentBlock>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import tw from 'twin.macro';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Field, Form, Formik, FormikHelpers } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||
|
@ -11,7 +11,7 @@ import { ApiKey } from '@/api/account/getApiKeys';
|
|||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Input, { Textarea } from '@/components/elements/Input';
|
||||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import ApiKeyModal from '@/components/dashboard/ApiKeyModal';
|
||||
|
||||
interface Values {
|
||||
|
@ -36,7 +36,7 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
|||
setApiKey(`${key.identifier}${secretToken}`);
|
||||
onKeyCreated(key);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
addError({ key: 'account', message: httpErrorToHuman(error) });
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import asDialog from '@/hoc/asDialog';
|
||||
import { Dialog, DialogWrapperContext } from '@/components/elements/dialog';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
|
@ -14,10 +15,10 @@ const DisableTOTPDialog = () => {
|
|||
const [password, setPassword] = useState('');
|
||||
const { clearAndAddHttpError } = useFlashKey('account:two-step');
|
||||
const { close, setProps } = useContext(DialogWrapperContext);
|
||||
const updateUserData = useStoreActions((actions) => actions.user.updateUserData);
|
||||
const updateUserData = useStoreActions(actions => actions.user.updateUserData);
|
||||
|
||||
useEffect(() => {
|
||||
setProps((state) => ({ ...state, preventExternalClose: submitting }));
|
||||
setProps(state => ({ ...state, preventExternalClose: submitting }));
|
||||
}, [submitting]);
|
||||
|
||||
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
|
@ -48,7 +49,7 @@ const DisableTOTPDialog = () => {
|
|||
type={'password'}
|
||||
variant={Input.Text.Variants.Loose}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
onChange={e => setPassword(e.currentTarget.value)}
|
||||
/>
|
||||
<Dialog.Footer>
|
||||
<Button.Text onClick={close}>Cancel</Button.Text>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { Dialog, DialogProps } from '@/components/elements/dialog';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
|
@ -30,7 +29,7 @@ export default ({ tokens, open, onClose }: RecoveryTokenDialogProps) => {
|
|||
<Dialog.Icon position={'container'} type={'success'} />
|
||||
<CopyOnClick text={tokens.join('\n')} showInNotification={false}>
|
||||
<pre className={'bg-gray-800 rounded p-2 mt-6'}>
|
||||
{grouped.map((value) => (
|
||||
{grouped.map(value => (
|
||||
<span key={value.join('_')} className={'block'}>
|
||||
{value[0]}
|
||||
<span className={'mx-2 selection:bg-gray-800'}> </span>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Dialog, DialogWrapperContext } from '@/components/elements/dialog';
|
||||
import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoFactorTokenData';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
|
@ -32,11 +33,11 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
|||
useEffect(() => {
|
||||
getTwoFactorTokenData()
|
||||
.then(setToken)
|
||||
.catch((error) => clearAndAddHttpError(error));
|
||||
.catch(error => clearAndAddHttpError(error));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setProps((state) => ({ ...state, preventExternalClose: submitting }));
|
||||
setProps(state => ({ ...state, preventExternalClose: submitting }));
|
||||
}, [submitting]);
|
||||
|
||||
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
|
@ -48,11 +49,11 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
|||
setSubmitting(true);
|
||||
clearAndAddHttpError();
|
||||
enableAccountTwoFactor(value, password)
|
||||
.then((tokens) => {
|
||||
.then(tokens => {
|
||||
updateUserData({ useTotp: true });
|
||||
onTokens(tokens);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
clearAndAddHttpError(error);
|
||||
setSubmitting(false);
|
||||
});
|
||||
|
@ -81,7 +82,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
|||
aria-labelledby={'totp-code-description'}
|
||||
variant={Input.Text.Variants.Loose}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
onChange={e => setValue(e.currentTarget.value)}
|
||||
className={'mt-3'}
|
||||
placeholder={'000000'}
|
||||
type={'text'}
|
||||
|
@ -97,7 +98,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
|||
className={'mt-1'}
|
||||
type={'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
onChange={e => setPassword(e.currentTarget.value)}
|
||||
/>
|
||||
<Dialog.Footer>
|
||||
<Button.Text onClick={close}>Cancel</Button.Text>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { Form, Formik, FormikHelpers } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
|
@ -34,15 +34,15 @@ export default () => {
|
|||
type: 'success',
|
||||
key: 'account:email',
|
||||
message: 'Your primary email has been updated.',
|
||||
})
|
||||
}),
|
||||
)
|
||||
.catch((error) =>
|
||||
.catch(error =>
|
||||
addFlash({
|
||||
type: 'error',
|
||||
key: 'account:email',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
resetForm();
|
||||
|
@ -53,7 +53,7 @@ export default () => {
|
|||
return (
|
||||
<Formik onSubmit={submit} validationSchema={schema} initialValues={{ email: user!.email, password: '' }}>
|
||||
{({ isSubmitting, isValid }) => (
|
||||
<React.Fragment>
|
||||
<Fragment>
|
||||
<SpinnerOverlay size={'large'} visible={isSubmitting} />
|
||||
<Form css={tw`m-0`}>
|
||||
<Field id={'current_email'} type={'email'} name={'email'} label={'Email'} />
|
||||
|
@ -69,7 +69,7 @@ export default () => {
|
|||
<Button disabled={isSubmitting || !isValid}>Update Email</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { Form, Formik, FormikHelpers } from 'formik';
|
||||
import Field from '@/components/elements/Field';
|
||||
|
@ -24,7 +24,7 @@ const schema = Yup.object().shape({
|
|||
'Password confirmation does not match the password you entered.',
|
||||
function (value) {
|
||||
return value === this.parent.password;
|
||||
}
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
|
@ -43,26 +43,26 @@ export default () => {
|
|||
// @ts-expect-error this is valid
|
||||
window.location = '/auth/login';
|
||||
})
|
||||
.catch((error) =>
|
||||
.catch(error =>
|
||||
addFlash({
|
||||
key: 'account:password',
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Fragment>
|
||||
<Formik
|
||||
onSubmit={submit}
|
||||
validationSchema={schema}
|
||||
initialValues={{ current: '', password: '', confirmPassword: '' }}
|
||||
>
|
||||
{({ isSubmitting, isValid }) => (
|
||||
<React.Fragment>
|
||||
<Fragment>
|
||||
<SpinnerOverlay size={'large'} visible={isSubmitting} />
|
||||
<Form css={tw`m-0`}>
|
||||
<Field
|
||||
|
@ -94,9 +94,9 @@ export default () => {
|
|||
<Button disabled={isSubmitting || !isValid}>Update Password</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
)}
|
||||
</Formik>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import useEventListener from '@/plugins/useEventListener';
|
||||
|
@ -18,7 +18,8 @@ export default () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{visible && <SearchModal appear visible={visible} onDismissed={() => setVisible(false)} />}
|
||||
<SearchModal appear visible={visible} onDismissed={() => setVisible(false)} />
|
||||
|
||||
<Tooltip placement={'bottom'} content={'Search'}>
|
||||
<div className={'navigation-link'} onClick={() => setVisible(true)}>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||
import { Field, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
|
@ -10,7 +10,7 @@ import getServers from '@/api/getServers';
|
|||
import { Server } from '@/api/server/getServer';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
import Input from '@/components/elements/Input';
|
||||
import { ip } from '@/lib/formatters';
|
||||
|
@ -47,10 +47,10 @@ const SearchWatcher = () => {
|
|||
|
||||
export default ({ ...props }: Props) => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const isAdmin = useStoreState((state) => state.user.data!.rootAdmin);
|
||||
const isAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
const { clearAndAddHttpError, clearFlashes } = useStoreActions(
|
||||
(actions: Actions<ApplicationStore>) => actions.flashes
|
||||
(actions: Actions<ApplicationStore>) => actions.flashes,
|
||||
);
|
||||
|
||||
const search = debounce(({ term }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
|
@ -58,8 +58,8 @@ export default ({ ...props }: Props) => {
|
|||
|
||||
// if (ref.current) ref.current.focus();
|
||||
getServers({ query: term, type: isAdmin ? 'admin-all' : undefined })
|
||||
.then((servers) => setServers(servers.items.filter((_, index) => index < 5)))
|
||||
.catch((error) => {
|
||||
.then(servers => setServers(servers.items.filter((_, index) => index < 5)))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'search', error });
|
||||
})
|
||||
|
@ -100,7 +100,7 @@ export default ({ ...props }: Props) => {
|
|||
</Form>
|
||||
{servers.length > 0 && (
|
||||
<div css={tw`mt-6`}>
|
||||
{servers.map((server) => (
|
||||
{servers.map(server => (
|
||||
<ServerResult
|
||||
key={server.uuid}
|
||||
to={`/server/${server.id}`}
|
||||
|
@ -110,8 +110,8 @@ export default ({ ...props }: Props) => {
|
|||
<p css={tw`text-sm`}>{server.name}</p>
|
||||
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
||||
{server.allocations
|
||||
.filter((alloc) => alloc.isDefault)
|
||||
.map((allocation) => (
|
||||
.filter(alloc => alloc.isDefault)
|
||||
.map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>
|
||||
{allocation.alias || ip(allocation.ip)}:{allocation.port}
|
||||
</span>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import ContentBox from '@/components/elements/ContentBox';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { Field, Form, Formik, FormikHelpers } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||
|
@ -6,7 +5,7 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
|||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Input, { Textarea } from '@/components/elements/Input';
|
||||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import { createSSHKey, useSSHKeys } from '@/api/account/ssh-keys';
|
||||
|
||||
|
@ -27,11 +26,11 @@ export default () => {
|
|||
clearAndAddHttpError();
|
||||
|
||||
createSSHKey(values.name, values.publicKey)
|
||||
.then((key) => {
|
||||
.then(key => {
|
||||
resetForm();
|
||||
mutate((data) => (data || []).concat(key));
|
||||
mutate(data => (data || []).concat(key));
|
||||
})
|
||||
.catch((error) => clearAndAddHttpError(error))
|
||||
.catch(error => clearAndAddHttpError(error))
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import tw from 'twin.macro';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import { deleteSSHKey, useSSHKeys } from '@/api/account/ssh-keys';
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
|
@ -16,9 +16,9 @@ export default ({ name, fingerprint }: { name: string; fingerprint: string }) =>
|
|||
clearAndAddHttpError();
|
||||
|
||||
Promise.all([
|
||||
mutate((data) => data?.filter((value) => value.fingerprint !== fingerprint), false),
|
||||
mutate(data => data?.filter(value => value.fingerprint !== fingerprint), false),
|
||||
deleteSSHKey(fingerprint),
|
||||
]).catch((error) => {
|
||||
]).catch(error => {
|
||||
mutate(undefined, true).catch(console.error);
|
||||
clearAndAddHttpError(error);
|
||||
});
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import React from 'react';
|
||||
import { Redirect, Route, RouteProps } from 'react-router';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useStoreState } from '@/state/hooks';
|
||||
|
||||
export default ({ children, ...props }: Omit<RouteProps, 'render'>) => {
|
||||
const isAuthenticated = useStoreState((state) => !!state.user.data?.uuid);
|
||||
function AuthenticatedRoute({ children }: { children?: ReactNode }): JSX.Element {
|
||||
const isAuthenticated = useStoreState(state => !!state.user.data?.uuid);
|
||||
|
||||
return (
|
||||
<Route
|
||||
{...props}
|
||||
render={({ location }) =>
|
||||
isAuthenticated ? children : <Redirect to={{ pathname: '/auth/login', state: { from: location } }} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const location = useLocation();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return <Navigate to="/auth/login" state={{ from: location.pathname }} />;
|
||||
}
|
||||
|
||||
export default AuthenticatedRoute;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
import * as React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
|
||||
|
@ -13,17 +13,17 @@ interface Props {
|
|||
const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
||||
${tw`relative inline-block rounded p-2 uppercase tracking-wide text-sm transition-all duration-150 border`};
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
((!props.isSecondary && !props.color) || props.color === 'primary') &&
|
||||
css<Props>`
|
||||
${(props) => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`};
|
||||
${props => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${tw`bg-primary-600 border-primary-700`};
|
||||
}
|
||||
`};
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.color === 'grey' &&
|
||||
css`
|
||||
${tw`border-neutral-600 bg-neutral-500 text-neutral-50`};
|
||||
|
@ -33,7 +33,7 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
|||
}
|
||||
`};
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.color === 'green' &&
|
||||
css<Props>`
|
||||
${tw`border-green-600 bg-green-500 text-green-50`};
|
||||
|
@ -42,7 +42,7 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
|||
${tw`bg-green-600 border-green-700`};
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.isSecondary &&
|
||||
css`
|
||||
&:active:not(:disabled) {
|
||||
|
@ -51,7 +51,7 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
|||
`};
|
||||
`};
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.color === 'red' &&
|
||||
css<Props>`
|
||||
${tw`border-red-600 bg-red-500 text-red-50`};
|
||||
|
@ -60,7 +60,7 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
|||
${tw`bg-red-600 border-red-700`};
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.isSecondary &&
|
||||
css`
|
||||
&:active:not(:disabled) {
|
||||
|
@ -69,21 +69,21 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
|||
`};
|
||||
`};
|
||||
|
||||
${(props) => props.size === 'xsmall' && tw`px-2 py-1 text-xs`};
|
||||
${(props) => (!props.size || props.size === 'small') && tw`px-4 py-2`};
|
||||
${(props) => props.size === 'large' && tw`p-4 text-sm`};
|
||||
${(props) => props.size === 'xlarge' && tw`p-4 w-full`};
|
||||
${props => props.size === 'xsmall' && tw`px-2 py-1 text-xs`};
|
||||
${props => (!props.size || props.size === 'small') && tw`px-4 py-2`};
|
||||
${props => props.size === 'large' && tw`p-4 text-sm`};
|
||||
${props => props.size === 'xlarge' && tw`p-4 w-full`};
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.isSecondary &&
|
||||
css<Props>`
|
||||
${tw`border-neutral-600 bg-transparent text-neutral-200`};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
${tw`border-neutral-500 text-neutral-100`};
|
||||
${(props) => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`};
|
||||
${(props) => props.color === 'primary' && tw`bg-primary-500 border-primary-600 text-primary-50`};
|
||||
${(props) => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`};
|
||||
${props => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`};
|
||||
${props => props.color === 'primary' && tw`bg-primary-500 border-primary-600 text-primary-50`};
|
||||
${props => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`};
|
||||
}
|
||||
`};
|
||||
|
||||
|
@ -108,7 +108,7 @@ const Button: React.FC<ComponentProps> = ({ children, isLoading, ...props }) =>
|
|||
|
||||
type LinkProps = Omit<JSX.IntrinsicElements['a'], 'ref' | keyof Props> & Props;
|
||||
|
||||
const LinkButton: React.FC<LinkProps> = (props) => <ButtonStyle as={'a'} {...props} />;
|
||||
const LinkButton: React.FC<LinkProps> = props => <ButtonStyle as={'a'} {...props} />;
|
||||
|
||||
export { LinkButton, ButtonStyle };
|
||||
export default Button;
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
import React, { memo } from 'react';
|
||||
import { usePermissions } from '@/plugins/usePermissions';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { usePermissions } from '@/plugins/usePermissions';
|
||||
|
||||
interface Props {
|
||||
action: string | string[];
|
||||
matchAny?: boolean;
|
||||
renderOnError?: React.ReactNode | null;
|
||||
children: React.ReactNode;
|
||||
renderOnError?: ReactNode | null;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Can = ({ action, matchAny = false, renderOnError, children }: Props) => {
|
||||
function Can({ action, matchAny = false, renderOnError, children }: Props) {
|
||||
const can = usePermissions(action);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(matchAny && can.filter((p) => p).length > 0) || (!matchAny && can.every((p) => p))
|
||||
? children
|
||||
: renderOnError}
|
||||
{(matchAny && can.filter(p => p).length > 0) || (!matchAny && can.every(p => p)) ? children : renderOnError}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default memo(Can, isEqual);
|
||||
const MemoizedCan = memo(Can, isEqual);
|
||||
|
||||
export default MemoizedCan;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { Field, FieldProps } from 'formik';
|
||||
import Input from '@/components/elements/Input';
|
||||
|
||||
|
@ -29,7 +28,7 @@ const Checkbox = ({ name, value, className, ...props }: Props & InputProps) => (
|
|||
type={'checkbox'}
|
||||
checked={(field.value || []).includes(value)}
|
||||
onClick={() => form.setFieldTouched(field.name, true)}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
const set = new Set(field.value);
|
||||
set.has(value) ? set.delete(value) : set.add(value);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface CodeProps {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import CodeMirror from 'codemirror';
|
||||
import styled from 'styled-components/macro';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
import modes from '@/modes';
|
||||
|
||||
require('codemirror/lib/codemirror.css');
|
||||
|
@ -106,7 +108,7 @@ const EditorContainer = styled.div`
|
|||
`;
|
||||
|
||||
export interface Props {
|
||||
style?: React.CSSProperties;
|
||||
style?: CSSProperties;
|
||||
initialContent?: string;
|
||||
mode: string;
|
||||
filename?: string;
|
||||
|
@ -119,7 +121,7 @@ const findModeByFilename = (filename: string) => {
|
|||
for (let i = 0; i < modes.length; i++) {
|
||||
const info = modes[i];
|
||||
|
||||
if (info.file && info.file.test(filename)) {
|
||||
if (info?.file !== undefined && info.file.test(filename)) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +132,7 @@ const findModeByFilename = (filename: string) => {
|
|||
if (ext) {
|
||||
for (let i = 0; i < modes.length; i++) {
|
||||
const info = modes[i];
|
||||
if (info.ext) {
|
||||
if (info?.ext !== undefined) {
|
||||
for (let j = 0; j < info.ext.length; j++) {
|
||||
if (info.ext[j] === ext) {
|
||||
return info;
|
||||
|
@ -146,10 +148,12 @@ const findModeByFilename = (filename: string) => {
|
|||
export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => {
|
||||
const [editor, setEditor] = useState<CodeMirror.Editor>();
|
||||
|
||||
const ref = useCallback((node) => {
|
||||
if (!node) return;
|
||||
const ref = useCallback<(_?: unknown) => void>(node => {
|
||||
if (node === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const e = CodeMirror.fromTextArea(node, {
|
||||
const e = CodeMirror.fromTextArea(node as HTMLTextAreaElement, {
|
||||
mode: 'text/plain',
|
||||
theme: 'ayu-mirage',
|
||||
indentUnit: 4,
|
||||
|
@ -158,7 +162,6 @@ export default ({ style, initialContent, filename, mode, fetchContent, onContent
|
|||
indentWithTabs: false,
|
||||
lineWrapping: true,
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
fixedGutter: true,
|
||||
scrollbarStyle: 'overlay',
|
||||
coverGutterNextToScrollbar: false,
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
import React, { useContext } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import asModal from '@/hoc/asModal';
|
||||
import ModalContext from '@/context/ModalContext';
|
||||
|
||||
type Props = {
|
||||
import Button from '@/components/elements/Button';
|
||||
import ModalContext from '@/context/ModalContext';
|
||||
import asModal from '@/hoc/asModal';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
||||
title: string;
|
||||
buttonText: string;
|
||||
onConfirmed: () => void;
|
||||
showSpinnerOverlay?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const ConfirmationModal: React.FC<Props> = ({ title, children, buttonText, onConfirmed }) => {
|
||||
function ConfirmationModal({ title, children, buttonText, onConfirmed }: Props) {
|
||||
const { dismiss } = useContext(ModalContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
|
||||
<div css={tw`text-neutral-300`}>{children}</div>
|
||||
|
||||
<div css={tw`flex flex-wrap items-center justify-end mt-8`}>
|
||||
<Button isSecondary onClick={() => dismiss()} css={tw`w-full sm:w-auto border-transparent`}>
|
||||
Cancel
|
||||
|
@ -28,10 +33,8 @@ const ConfirmationModal: React.FC<Props> = ({ title, children, buttonText, onCon
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
ConfirmationModal.displayName = 'ConfirmationModal';
|
||||
|
||||
export default asModal<Props>((props) => ({
|
||||
export default asModal<Props>(props => ({
|
||||
showSpinnerOverlay: props.showSpinnerOverlay,
|
||||
}))(ConfirmationModal);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import tw from 'twin.macro';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import { breakpoint } from '@/theme';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
import Portal from '@/components/elements/Portal';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import classNames from 'classnames';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import type { MouseEvent, ReactNode } from 'react';
|
||||
import { Children, cloneElement, isValidElement, useEffect, useState } from 'react';
|
||||
|
||||
import Portal from '@/components/elements/Portal';
|
||||
import FadeTransition from '@/components/elements/transitions/FadeTransition';
|
||||
|
||||
interface CopyOnClickProps {
|
||||
text: string | number | null | undefined;
|
||||
showInNotification?: boolean;
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const CopyOnClick = ({ text, showInNotification = true, children }: CopyOnClickProps) => {
|
||||
|
@ -25,15 +27,16 @@ const CopyOnClick = ({ text, showInNotification = true, children }: CopyOnClickP
|
|||
};
|
||||
}, [copied]);
|
||||
|
||||
if (!React.isValidElement(children)) {
|
||||
if (!isValidElement(children)) {
|
||||
throw new Error('Component passed to <CopyOnClick/> must be a valid React element.');
|
||||
}
|
||||
|
||||
const child = !text
|
||||
? React.Children.only(children)
|
||||
: React.cloneElement(React.Children.only(children), {
|
||||
? Children.only(children)
|
||||
: cloneElement(Children.only(children), {
|
||||
// @ts-expect-error I don't know
|
||||
className: classNames(children.props.className || '', 'cursor-pointer'),
|
||||
onClick: (e: React.MouseEvent<HTMLElement>) => {
|
||||
onClick: (e: MouseEvent<HTMLElement>) => {
|
||||
copy(String(text));
|
||||
setCopied(true);
|
||||
if (typeof children.props.onClick === 'function') {
|
||||
|
@ -46,9 +49,9 @@ const CopyOnClick = ({ text, showInNotification = true, children }: CopyOnClickP
|
|||
<>
|
||||
{copied && (
|
||||
<Portal>
|
||||
<Fade in appear timeout={250} key={copied ? 'visible' : 'invisible'}>
|
||||
<div className={'fixed z-50 bottom-0 right-0 m-4'}>
|
||||
<div className={'rounded-md py-3 px-4 text-gray-200 bg-neutral-600/95 shadow'}>
|
||||
<FadeTransition show duration="duration-250" key={copied ? 'visible' : 'invisible'}>
|
||||
<div className="fixed z-50 bottom-0 right-0 m-4">
|
||||
<div className="rounded-md py-3 px-4 text-gray-200 bg-neutral-600/95 shadow">
|
||||
<p>
|
||||
{showInNotification
|
||||
? `Copied "${String(text)}" to clipboard.`
|
||||
|
@ -56,7 +59,7 @@ const CopyOnClick = ({ text, showInNotification = true, children }: CopyOnClickP
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Fade>
|
||||
</FadeTransition>
|
||||
</Portal>
|
||||
)}
|
||||
{child}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import React, { createRef } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
||||
import { createRef, PureComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
|
||||
import FadeTransition from '@/components/elements/transitions/FadeTransition';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
renderToggle: (onClick: (e: React.MouseEvent<any, MouseEvent>) => void) => React.ReactChild;
|
||||
children: ReactNode;
|
||||
renderToggle: (onClick: (e: ReactMouseEvent<unknown>) => void) => any;
|
||||
}
|
||||
|
||||
export const DropdownButtonRow = styled.button<{ danger?: boolean }>`
|
||||
|
@ -13,7 +15,7 @@ export const DropdownButtonRow = styled.button<{ danger?: boolean }>`
|
|||
transition: 150ms all ease;
|
||||
|
||||
&:hover {
|
||||
${(props) => (props.danger ? tw`text-red-700 bg-red-100` : tw`text-neutral-700 bg-neutral-100`)};
|
||||
${props => (props.danger ? tw`text-red-700 bg-red-100` : tw`text-neutral-700 bg-neutral-100`)};
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -22,19 +24,19 @@ interface State {
|
|||
visible: boolean;
|
||||
}
|
||||
|
||||
class DropdownMenu extends React.PureComponent<Props, State> {
|
||||
class DropdownMenu extends PureComponent<Props, State> {
|
||||
menu = createRef<HTMLDivElement>();
|
||||
|
||||
state: State = {
|
||||
override state: State = {
|
||||
posX: 0,
|
||||
visible: false,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
override componentWillUnmount() {
|
||||
this.removeListeners();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||
override componentDidUpdate(_prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||
const menu = this.menu.current;
|
||||
|
||||
if (this.state.visible && !prevState.visible && menu) {
|
||||
|
@ -48,19 +50,21 @@ class DropdownMenu extends React.PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
removeListeners = () => {
|
||||
removeListeners() {
|
||||
document.removeEventListener('click', this.windowListener);
|
||||
document.removeEventListener('contextmenu', this.contextMenuListener);
|
||||
};
|
||||
}
|
||||
|
||||
onClickHandler = (e: React.MouseEvent<any, MouseEvent>) => {
|
||||
onClickHandler(e: ReactMouseEvent<unknown>) {
|
||||
e.preventDefault();
|
||||
this.triggerMenu(e.clientX);
|
||||
};
|
||||
}
|
||||
|
||||
contextMenuListener = () => this.setState({ visible: false });
|
||||
contextMenuListener() {
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
|
||||
windowListener = (e: MouseEvent) => {
|
||||
windowListener(e: MouseEvent): any {
|
||||
const menu = this.menu.current;
|
||||
|
||||
if (e.button === 2 || !this.state.visible || !menu) {
|
||||
|
@ -74,22 +78,24 @@ class DropdownMenu extends React.PureComponent<Props, State> {
|
|||
if (e.target !== menu && !menu.contains(e.target as Node)) {
|
||||
this.setState({ visible: false });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
triggerMenu = (posX: number) =>
|
||||
this.setState((s) => ({
|
||||
triggerMenu(posX: number) {
|
||||
this.setState(s => ({
|
||||
posX: !s.visible ? posX : s.posX,
|
||||
visible: !s.visible,
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
override render() {
|
||||
return (
|
||||
<div>
|
||||
{this.props.renderToggle(this.onClickHandler)}
|
||||
<Fade timeout={150} in={this.state.visible} unmountOnExit>
|
||||
|
||||
<FadeTransition duration="duration-150" show={this.state.visible} appear unmount>
|
||||
<div
|
||||
ref={this.menu}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
this.setState({ visible: false });
|
||||
}}
|
||||
|
@ -98,7 +104,7 @@ class DropdownMenu extends React.PureComponent<Props, State> {
|
|||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Fade>
|
||||
</FadeTransition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import React from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import Icon from '@/components/elements/Icon';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Component } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
import Icon from '@/components/elements/Icon';
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
class ErrorBoundary extends React.Component<{}, State> {
|
||||
state: State = {
|
||||
class ErrorBoundary extends Component<Props, State> {
|
||||
override state: State = {
|
||||
hasError: false,
|
||||
};
|
||||
|
||||
|
@ -17,15 +22,16 @@ class ErrorBoundary extends React.Component<{}, State> {
|
|||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
override componentDidCatch(error: Error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
render() {
|
||||
override render() {
|
||||
return this.state.hasError ? (
|
||||
<div css={tw`flex items-center justify-center w-full my-4`}>
|
||||
<div css={tw`flex items-center bg-neutral-900 rounded p-3 text-red-500`}>
|
||||
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`} />
|
||||
|
||||
<p css={tw`text-sm text-neutral-100`}>
|
||||
An error was encountered by the application while rendering this view. Try refreshing the page.
|
||||
</p>
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import React from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import styled from 'styled-components/macro';
|
||||
import CSSTransition, { CSSTransitionProps } from 'react-transition-group/CSSTransition';
|
||||
|
||||
interface Props extends Omit<CSSTransitionProps, 'timeout' | 'classNames'> {
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
const Container = styled.div<{ timeout: number }>`
|
||||
.fade-enter,
|
||||
.fade-exit,
|
||||
.fade-appear {
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-appear {
|
||||
${tw`opacity-0`};
|
||||
|
||||
&.fade-enter-active,
|
||||
&.fade-appear-active {
|
||||
${tw`opacity-100 transition-opacity ease-in`};
|
||||
transition-duration: ${(props) => props.timeout}ms;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-exit {
|
||||
${tw`opacity-100`};
|
||||
|
||||
&.fade-exit-active {
|
||||
${tw`opacity-0 transition-opacity ease-in`};
|
||||
transition-duration: ${(props) => props.timeout}ms;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Fade: React.FC<Props> = ({ timeout, children, ...props }) => (
|
||||
<Container timeout={timeout}>
|
||||
<CSSTransition timeout={timeout} classNames={'fade'} {...props}>
|
||||
{children}
|
||||
</CSSTransition>
|
||||
</Container>
|
||||
);
|
||||
Fade.displayName = 'Fade';
|
||||
|
||||
export default Fade;
|
|
@ -1,4 +1,5 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Field as FormikField, FieldProps } from 'formik';
|
||||
import Input from '@/components/elements/Input';
|
||||
import Label from '@/components/elements/Label';
|
||||
|
@ -41,7 +42,7 @@ const Field = forwardRef<HTMLInputElement, Props>(
|
|||
</div>
|
||||
)}
|
||||
</FormikField>
|
||||
)
|
||||
),
|
||||
);
|
||||
Field.displayName = 'Field';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Field, FieldProps } from 'formik';
|
||||
import InputError from '@/components/elements/InputError';
|
||||
import Label from '@/components/elements/Label';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||
import { Field, FieldProps } from 'formik';
|
||||
import Switch, { SwitchProps } from '@/components/elements/Switch';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
export default styled.div<{ $hoverable?: boolean }>`
|
||||
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150 overflow-hidden`};
|
||||
|
||||
${(props) => props.$hoverable !== false && tw`hover:border-neutral-500`};
|
||||
${props => props.$hoverable !== false && tw`hover:border-neutral-500`};
|
||||
|
||||
& .icon {
|
||||
${tw`rounded-full w-16 flex items-center justify-center bg-neutral-500 p-3`};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { CSSProperties } from 'react';
|
||||
import { CSSProperties } from 'react';
|
||||
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import styled, { css } from 'styled-components/macro';
|
||||
import styled, { css } from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
export interface Props {
|
||||
|
@ -45,7 +45,7 @@ const inputStyle = css<Props>`
|
|||
|
||||
& + .input-help {
|
||||
${tw`mt-1 text-xs`};
|
||||
${(props) => (props.hasError ? tw`text-red-200` : tw`text-neutral-200`)};
|
||||
${props => (props.hasError ? tw`text-red-200` : tw`text-neutral-200`)};
|
||||
}
|
||||
|
||||
&:required,
|
||||
|
@ -55,15 +55,15 @@ const inputStyle = css<Props>`
|
|||
|
||||
&:not(:disabled):not(:read-only):focus {
|
||||
${tw`shadow-md border-primary-300 ring-2 ring-primary-400 ring-opacity-50`};
|
||||
${(props) => props.hasError && tw`border-red-300 ring-red-200`};
|
||||
${props => props.hasError && tw`border-red-300 ring-red-200`};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
${tw`opacity-75`};
|
||||
}
|
||||
|
||||
${(props) => props.isLight && light};
|
||||
${(props) => props.hasError && tw`text-red-100 border-red-400 hover:border-red-300`};
|
||||
${props => props.isLight && light};
|
||||
${props => props.hasError && tw`text-red-100 border-red-400 hover:border-red-300`};
|
||||
`;
|
||||
|
||||
const Input = styled.input<Props>`
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { FormikErrors, FormikTouched } from 'formik';
|
||||
import tw from 'twin.macro';
|
||||
import { capitalize } from '@/lib/strings';
|
||||
|
@ -15,7 +14,7 @@ const InputError = ({ errors, touched, name, children }: Props) =>
|
|||
<p css={tw`text-xs text-red-400 pt-2`}>
|
||||
{typeof errors[name] === 'string'
|
||||
? capitalize(errors[name] as string)
|
||||
: capitalize((errors[name] as unknown as string[])[0])}
|
||||
: capitalize((errors[name] as unknown as string[])[0] ?? '')}
|
||||
</p>
|
||||
) : (
|
||||
<>{children ? <p css={tw`text-xs text-neutral-400 pt-2`}>{children}</p> : null}</>
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import React from 'react';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
import type { ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
|
||||
import Select from '@/components/elements/Select';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import FadeTransition from '@/components/elements/transitions/FadeTransition';
|
||||
|
||||
const Container = styled.div<{ visible?: boolean }>`
|
||||
${tw`relative`};
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.visible &&
|
||||
css`
|
||||
& ${Select} {
|
||||
|
@ -17,15 +18,18 @@ const Container = styled.div<{ visible?: boolean }>`
|
|||
`};
|
||||
`;
|
||||
|
||||
const InputSpinner = ({ visible, children }: { visible: boolean; children: React.ReactNode }) => (
|
||||
<Container visible={visible}>
|
||||
<Fade appear unmountOnExit in={visible} timeout={150}>
|
||||
<div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}>
|
||||
<Spinner size={'small'} />
|
||||
</div>
|
||||
</Fade>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
function InputSpinner({ visible, children }: { visible: boolean; children: ReactNode }) {
|
||||
return (
|
||||
<Container visible={visible}>
|
||||
<FadeTransition show={visible} duration="duration-150" appear unmount>
|
||||
<div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}>
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
</FadeTransition>
|
||||
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default InputSpinner;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
const Label = styled.label<{ isLight?: boolean }>`
|
||||
${tw`block text-xs uppercase text-neutral-200 mb-1 sm:mb-2`};
|
||||
${(props) => props.isLight && tw`text-neutral-700`};
|
||||
${props => props.isLight && tw`text-neutral-700`};
|
||||
`;
|
||||
|
||||
export default Label;
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import tw from 'twin.macro';
|
||||
import styled, { css } from 'styled-components/macro';
|
||||
import { breakpoint } from '@/theme';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styled, { css } from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { breakpoint } from '@/theme';
|
||||
import FadeTransition from '@/components/elements/transitions/FadeTransition';
|
||||
|
||||
export interface RequiredModalProps {
|
||||
children?: ReactNode;
|
||||
|
||||
visible: boolean;
|
||||
onDismissed: () => void;
|
||||
appear?: boolean;
|
||||
|
@ -32,7 +36,7 @@ const ModalContainer = styled.div<{ alignTop?: boolean }>`
|
|||
${breakpoint('lg')`max-width: 50%`};
|
||||
|
||||
${tw`relative flex flex-col w-full m-auto`};
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.alignTop &&
|
||||
css`
|
||||
margin-top: 20%;
|
||||
|
@ -55,7 +59,7 @@ const ModalContainer = styled.div<{ alignTop?: boolean }>`
|
|||
}
|
||||
`;
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
function Modal({
|
||||
visible,
|
||||
appear,
|
||||
dismissable,
|
||||
|
@ -65,7 +69,7 @@ const Modal: React.FC<ModalProps> = ({
|
|||
closeOnEscape = true,
|
||||
onDismissed,
|
||||
children,
|
||||
}) => {
|
||||
}: ModalProps) {
|
||||
const [render, setRender] = useState(visible);
|
||||
|
||||
const isDismissable = useMemo(() => {
|
||||
|
@ -85,14 +89,20 @@ const Modal: React.FC<ModalProps> = ({
|
|||
};
|
||||
}, [isDismissable, closeOnEscape, render]);
|
||||
|
||||
useEffect(() => setRender(visible), [visible]);
|
||||
useEffect(() => {
|
||||
setRender(visible);
|
||||
|
||||
if (!visible) {
|
||||
onDismissed();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<Fade in={render} timeout={150} appear={appear || true} unmountOnExit onExited={() => onDismissed()}>
|
||||
<FadeTransition as={Fragment} show={render} duration="duration-150" appear={appear ?? true} unmount>
|
||||
<ModalMask
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onContextMenu={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => {
|
||||
onClick={e => e.stopPropagation()}
|
||||
onContextMenu={e => e.stopPropagation()}
|
||||
onMouseDown={e => {
|
||||
if (isDismissable && closeOnBackground) {
|
||||
e.stopPropagation();
|
||||
if (e.target === e.currentTarget) {
|
||||
|
@ -119,16 +129,16 @@ const Modal: React.FC<ModalProps> = ({
|
|||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{showSpinnerOverlay && (
|
||||
<Fade timeout={150} appear in>
|
||||
<div
|
||||
css={tw`absolute w-full h-full rounded flex items-center justify-center`}
|
||||
style={{ background: 'hsla(211, 10%, 53%, 0.35)', zIndex: 9999 }}
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
</Fade>
|
||||
)}
|
||||
|
||||
<FadeTransition duration="duration-150" show={showSpinnerOverlay ?? false} appear>
|
||||
<div
|
||||
css={tw`absolute w-full h-full rounded flex items-center justify-center`}
|
||||
style={{ background: 'hsla(211, 10%, 53%, 0.35)', zIndex: 9999 }}
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
</FadeTransition>
|
||||
|
||||
<div
|
||||
css={tw`bg-neutral-800 p-3 sm:p-4 md:p-6 rounded shadow-md overflow-y-scroll transition-all duration-150`}
|
||||
>
|
||||
|
@ -136,14 +146,14 @@ const Modal: React.FC<ModalProps> = ({
|
|||
</div>
|
||||
</ModalContainer>
|
||||
</ModalMask>
|
||||
</Fade>
|
||||
</FadeTransition>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const PortaledModal: React.FC<ModalProps> = ({ children, ...props }) => {
|
||||
function PortaledModal({ children, ...props }: ModalProps): JSX.Element {
|
||||
const element = useRef(document.getElementById('modal-portal'));
|
||||
|
||||
return createPortal(<Modal {...props}>{children}</Modal>, element.current!);
|
||||
};
|
||||
}
|
||||
|
||||
export default PortaledModal;
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import ContentContainer from '@/components/elements/ContentContainer';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
import ContentContainer from '@/components/elements/ContentContainer';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
||||
export interface PageContentBlockProps {
|
||||
children?: ReactNode;
|
||||
|
||||
title?: string;
|
||||
className?: string;
|
||||
showFlashKey?: string;
|
||||
}
|
||||
|
||||
const PageContentBlock: React.FC<PageContentBlockProps> = ({ title, showFlashKey, className, children }) => {
|
||||
function PageContentBlock({ title, showFlashKey, className, children }: PageContentBlockProps) {
|
||||
useEffect(() => {
|
||||
if (title) {
|
||||
document.title = title;
|
||||
|
@ -18,28 +21,27 @@ const PageContentBlock: React.FC<PageContentBlockProps> = ({ title, showFlashKey
|
|||
}, [title]);
|
||||
|
||||
return (
|
||||
<CSSTransition timeout={150} classNames={'fade'} appear in>
|
||||
<>
|
||||
<ContentContainer css={tw`my-4 sm:my-10`} className={className}>
|
||||
{showFlashKey && <FlashMessageRender byKey={showFlashKey} css={tw`mb-4`} />}
|
||||
{children}
|
||||
</ContentContainer>
|
||||
<ContentContainer css={tw`mb-4`}>
|
||||
<p css={tw`text-center text-neutral-500 text-xs`}>
|
||||
<a
|
||||
rel={'noopener nofollow noreferrer'}
|
||||
href={'https://pterodactyl.io'}
|
||||
target={'_blank'}
|
||||
css={tw`no-underline text-neutral-500 hover:text-neutral-300`}
|
||||
>
|
||||
Pterodactyl®
|
||||
</a>
|
||||
© 2015 - {new Date().getFullYear()}
|
||||
</p>
|
||||
</ContentContainer>
|
||||
</>
|
||||
</CSSTransition>
|
||||
<>
|
||||
<ContentContainer css={tw`my-4 sm:my-10`} className={className}>
|
||||
{showFlashKey && <FlashMessageRender byKey={showFlashKey} css={tw`mb-4`} />}
|
||||
{children}
|
||||
</ContentContainer>
|
||||
|
||||
<ContentContainer css={tw`mb-4`}>
|
||||
<p css={tw`text-center text-neutral-500 text-xs`}>
|
||||
<a
|
||||
rel={'noopener nofollow noreferrer'}
|
||||
href={'https://pterodactyl.io'}
|
||||
target={'_blank'}
|
||||
css={tw`no-underline text-neutral-500 hover:text-neutral-300`}
|
||||
>
|
||||
Pterodactyl®
|
||||
</a>
|
||||
© 2015 - {new Date().getFullYear()}
|
||||
</p>
|
||||
</ContentContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default PageContentBlock;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { PaginatedResult } from '@/api/http';
|
||||
import tw from 'twin.macro';
|
||||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import Button from '@/components/elements/Button';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAngleDoubleLeft, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
|
@ -48,12 +48,12 @@ function Pagination<T>({ data: { items, pagination }, onPageSelect, children }:
|
|||
{children({ items, isFirstPage, isLastPage })}
|
||||
{pages.length > 1 && (
|
||||
<div css={tw`mt-4 flex justify-center`}>
|
||||
{pages[0] > 1 && !isFirstPage && (
|
||||
{(pages?.[0] ?? 0) > 1 && !isFirstPage && (
|
||||
<Block isSecondary color={'primary'} onClick={() => onPageSelect(1)}>
|
||||
<FontAwesomeIcon icon={faAngleDoubleLeft} />
|
||||
</Block>
|
||||
)}
|
||||
{pages.map((i) => (
|
||||
{pages.map(i => (
|
||||
<Block
|
||||
isSecondary={pagination.currentPage !== i}
|
||||
color={'primary'}
|
||||
|
@ -63,7 +63,7 @@ function Pagination<T>({ data: { items, pagination }, onPageSelect, children }:
|
|||
{i}
|
||||
</Block>
|
||||
))}
|
||||
{pages[4] < pagination.totalPages && !isLastPage && (
|
||||
{(pages?.[4] ?? 0) < pagination.totalPages && !isLastPage && (
|
||||
<Block isSecondary color={'primary'} onClick={() => onPageSelect(pagination.totalPages)}>
|
||||
<FontAwesomeIcon icon={faAngleDoubleRight} />
|
||||
</Block>
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { RouteProps } from 'react-router';
|
||||
import Can from '@/components/elements/Can';
|
||||
import { ServerError } from '@/components/elements/ScreenBlock';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Props extends Omit<RouteProps, 'path'> {
|
||||
path: string;
|
||||
permission: string | string[] | null;
|
||||
import { ServerError } from '@/components/elements/ScreenBlock';
|
||||
import { usePermissions } from '@/plugins/usePermissions';
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
|
||||
permission?: string | string[];
|
||||
}
|
||||
|
||||
export default ({ permission, children, ...props }: Props) => (
|
||||
<Route {...props}>
|
||||
{!permission ? (
|
||||
children
|
||||
) : (
|
||||
<Can
|
||||
matchAny
|
||||
action={permission}
|
||||
renderOnError={
|
||||
<ServerError title={'Access Denied'} message={'You do not have permission to access this page.'} />
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Can>
|
||||
)}
|
||||
</Route>
|
||||
);
|
||||
function PermissionRoute({ children, permission }: Props): JSX.Element {
|
||||
if (permission === undefined) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
const can = usePermissions(permission);
|
||||
|
||||
if (can.filter(p => p).length > 0) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return <ServerError title="Access Denied" message="You do not have permission to access this page." />;
|
||||
}
|
||||
|
||||
export default PermissionRoute;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
export default ({ children }: { children: React.ReactNode }) => {
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { randomInt } from '@/helpers';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import tw from 'twin.macro';
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const BarFill = styled.div`
|
||||
${tw`h-full bg-cyan-400`};
|
||||
transition: 250ms ease-in-out;
|
||||
box-shadow: 0 -2px 10px 2px hsl(178, 78%, 57%);
|
||||
`;
|
||||
import { randomInt } from '@/helpers';
|
||||
|
||||
type Timer = ReturnType<typeof setTimeout>;
|
||||
|
||||
export default () => {
|
||||
const interval = useRef<Timer>(null) as React.MutableRefObject<Timer>;
|
||||
const timeout = useRef<Timer>(null) as React.MutableRefObject<Timer>;
|
||||
function ProgressBar() {
|
||||
const interval = useRef<Timer>();
|
||||
const timeout = useRef<Timer>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const progress = useStoreState((state) => state.progress.progress);
|
||||
const continuous = useStoreState((state) => state.progress.continuous);
|
||||
const setProgress = useStoreActions((actions) => actions.progress.setProgress);
|
||||
|
||||
const continuous = useStoreState(state => state.progress.continuous);
|
||||
const progress = useStoreState(state => state.progress.progress);
|
||||
const setProgress = useStoreActions(actions => actions.progress.setProgress);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
@ -59,10 +53,26 @@ export default () => {
|
|||
}, [progress, continuous]);
|
||||
|
||||
return (
|
||||
<div css={tw`w-full fixed`} style={{ height: '2px' }}>
|
||||
<CSSTransition timeout={150} appear in={visible} unmountOnExit classNames={'fade'}>
|
||||
<BarFill style={{ width: progress === undefined ? '100%' : `${progress}%` }} />
|
||||
</CSSTransition>
|
||||
<div className="fixed h-[2px] w-full">
|
||||
<Transition
|
||||
as={Fragment}
|
||||
show={visible}
|
||||
enter="transition-opacity duration-150"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
appear
|
||||
unmount
|
||||
>
|
||||
<div
|
||||
className="h-full bg-cyan-400 shadow-[0_-2px_10px_2px] shadow-[#3CE7E1] transition-all duration-[250ms] ease-in-out"
|
||||
style={{ width: progress === undefined ? '100%' : `${progress}%` }}
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ProgressBar;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faArrowLeft, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import styled, { keyframes } from 'styled-components/macro';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import NotFoundSvg from '@/assets/images/not_found.svg';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import styled, { css } from 'styled-components/macro';
|
||||
import styled, { css } from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
interface Props {
|
||||
|
@ -25,7 +25,7 @@ const Select = styled.select<Props>`
|
|||
display: none;
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
!props.hideDropdownArrow &&
|
||||
css`
|
||||
${tw`bg-neutral-600 border-neutral-500 text-neutral-200`};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import PageContentBlock, { PageContentBlockProps } from '@/components/elements/PageContentBlock';
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
interface Props extends PageContentBlockProps {
|
||||
|
@ -7,7 +7,7 @@ interface Props extends PageContentBlockProps {
|
|||
}
|
||||
|
||||
const ServerContentBlock: React.FC<Props> = ({ title, children, ...props }) => {
|
||||
const name = ServerContext.useStoreState((state) => state.server.data!.name);
|
||||
const name = ServerContext.useStoreState(state => state.server.data!.name);
|
||||
|
||||
return (
|
||||
<PageContentBlock title={`${name} | ${title}`} {...props}>
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import React, { Suspense } from 'react';
|
||||
import styled, { css, keyframes } from 'styled-components/macro';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||
|
||||
export type SpinnerSize = 'small' | 'base' | 'large';
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
|
||||
size?: SpinnerSize;
|
||||
centered?: boolean;
|
||||
isBlue?: boolean;
|
||||
}
|
||||
|
||||
interface Spinner extends React.FC<Props> {
|
||||
interface Spinner extends FC<Props> {
|
||||
Size: Record<'SMALL' | 'BASE' | 'LARGE', SpinnerSize>;
|
||||
Suspense: React.FC<Props>;
|
||||
Suspense: FC<Props>;
|
||||
}
|
||||
|
||||
const spin = keyframes`
|
||||
|
@ -27,7 +31,7 @@ const SpinnerComponent = styled.div<Props>`
|
|||
border-radius: 50%;
|
||||
animation: ${spin} 1s cubic-bezier(0.55, 0.25, 0.25, 0.7) infinite;
|
||||
|
||||
${(props) =>
|
||||
${props =>
|
||||
props.size === 'small'
|
||||
? tw`w-4 h-4 border-2`
|
||||
: props.size === 'large'
|
||||
|
@ -37,8 +41,8 @@ const SpinnerComponent = styled.div<Props>`
|
|||
`
|
||||
: null};
|
||||
|
||||
border-color: ${(props) => (!props.isBlue ? 'rgba(255, 255, 255, 0.2)' : 'hsla(212, 92%, 43%, 0.2)')};
|
||||
border-top-color: ${(props) => (!props.isBlue ? 'rgb(255, 255, 255)' : 'hsl(212, 92%, 43%)')};
|
||||
border-color: ${props => (!props.isBlue ? 'rgba(255, 255, 255, 0.2)' : 'hsla(212, 92%, 43%, 0.2)')};
|
||||
border-top-color: ${props => (!props.isBlue ? 'rgb(255, 255, 255)' : 'hsl(212, 92%, 43%)')};
|
||||
`;
|
||||
|
||||
const Spinner: Spinner = ({ centered, ...props }) =>
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
import React from 'react';
|
||||
import Spinner, { SpinnerSize } from '@/components/elements/Spinner';
|
||||
import Fade from '@/components/elements/Fade';
|
||||
import type { ReactNode } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
import Spinner, { SpinnerSize } from '@/components/elements/Spinner';
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
|
||||
visible: boolean;
|
||||
fixed?: boolean;
|
||||
size?: SpinnerSize;
|
||||
backgroundOpacity?: number;
|
||||
}
|
||||
|
||||
const SpinnerOverlay: React.FC<Props> = ({ size, fixed, visible, backgroundOpacity, children }) => (
|
||||
<Fade timeout={150} in={visible} unmountOnExit>
|
||||
function SpinnerOverlay({ size, fixed, visible, backgroundOpacity, children }: Props) {
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
css={[
|
||||
tw`top-0 left-0 flex items-center justify-center w-full h-full rounded flex-col z-40`,
|
||||
|
@ -22,7 +28,7 @@ const SpinnerOverlay: React.FC<Props> = ({ size, fixed, visible, backgroundOpaci
|
|||
<Spinner size={size} />
|
||||
{children && (typeof children === 'string' ? <p css={tw`mt-4 text-neutral-400`}>{children}</p> : children)}
|
||||
</div>
|
||||
</Fade>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default SpinnerOverlay;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import styled from 'styled-components/macro';
|
||||
import styled from 'styled-components';
|
||||
import tw, { theme } from 'twin.macro';
|
||||
|
||||
const SubNavigation = styled.div`
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { v4 } from 'uuid';
|
||||
import type { ChangeEvent, ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import styled from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
import Label from '@/components/elements/Label';
|
||||
import Input from '@/components/elements/Input';
|
||||
|
@ -42,12 +43,12 @@ export interface SwitchProps {
|
|||
description?: string;
|
||||
defaultChecked?: boolean;
|
||||
readOnly?: boolean;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
children?: React.ReactNode;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Switch = ({ name, label, description, defaultChecked, readOnly, onChange, children }: SwitchProps) => {
|
||||
const uuid = useMemo(() => v4(), []);
|
||||
const uuid = useMemo(() => nanoid(), []);
|
||||
|
||||
return (
|
||||
<div css={tw`flex items-center`}>
|
||||
|
@ -57,7 +58,7 @@ const Switch = ({ name, label, description, defaultChecked, readOnly, onChange,
|
|||
id={uuid}
|
||||
name={name}
|
||||
type={'checkbox'}
|
||||
onChange={(e) => onChange && onChange(e)}
|
||||
onChange={e => onChange && onChange(e)}
|
||||
defaultChecked={defaultChecked}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { memo } from 'react';
|
||||
import { memo } from 'react';
|
||||
import * as React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import tw from 'twin.macro';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Trans, TransProps, useTranslation } from 'react-i18next';
|
||||
import type { TransProps } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = Omit<TransProps, 't'>;
|
||||
type Props = Omit<TransProps<string, string>, 't'>;
|
||||
|
||||
export default ({ ns, children, ...props }: Props) => {
|
||||
function Translate({ ns, children, ...props }: Props) {
|
||||
const { t } = useTranslation(ns);
|
||||
|
||||
return (
|
||||
|
@ -11,4 +11,6 @@ export default ({ ns, children, ...props }: Props) => {
|
|||
{children}
|
||||
</Trans>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Translate;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Tooltip from '@/components/elements/tooltip/Tooltip';
|
||||
import Translate from '@/components/elements/Translate';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { ClipboardListIcon } from '@heroicons/react/outline';
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ExclamationIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface AlertProps {
|
||||
|
@ -17,7 +17,7 @@ export default ({ type, className, children }: AlertProps) => {
|
|||
['border-red-500 bg-red-500/25']: type === 'danger',
|
||||
['border-yellow-500 bg-yellow-500/25']: type === 'warning',
|
||||
},
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{type === 'danger' ? (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ButtonProps, Options } from '@/components/elements/button/types';
|
||||
import styles from './style.module.css';
|
||||
|
@ -17,14 +17,14 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
[styles.small]: size === Options.Size.Small,
|
||||
[styles.large]: size === Options.Size.Large,
|
||||
},
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const TextButton = forwardRef<HTMLButtonElement, ButtonProps>(({ className, ...props }, ref) => (
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
enum Shape {
|
||||
export enum Shape {
|
||||
Default,
|
||||
IconSquare,
|
||||
}
|
||||
|
||||
enum Size {
|
||||
export enum Size {
|
||||
Default,
|
||||
Small,
|
||||
Large,
|
||||
}
|
||||
|
||||
enum Variant {
|
||||
export enum Variant {
|
||||
Primary,
|
||||
Secondary,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Dialog, RenderDialogProps } from './';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Dialog as HDialog } from '@headlessui/react';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import { XIcon } from '@heroicons/react/solid';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import * as React from 'react';
|
||||
import { DialogContext } from './';
|
||||
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
|
||||
|
||||
|
@ -7,7 +8,7 @@ export default ({ children }: { children: React.ReactNode }) => {
|
|||
|
||||
useDeepCompareEffect(() => {
|
||||
setFooter(
|
||||
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>{children}</div>
|
||||
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>{children}</div>,
|
||||
);
|
||||
}, [children]);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext, useEffect } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
|
||||
import classNames from 'classnames';
|
||||
import { DialogContext, DialogIconProps, styles } from './';
|
||||
|
@ -19,7 +19,7 @@ export default ({ type, position, className }: DialogIconProps) => {
|
|||
setIcon(
|
||||
<div className={classNames(styles.dialog_icon, styles[type], className)}>
|
||||
<Icon className={'w-6 h-6'} />
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
}, [type, className]);
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react';
|
||||
import { createContext } from 'react';
|
||||
import { DialogContextType, DialogWrapperContextType } from './types';
|
||||
|
||||
export const DialogContext = React.createContext<DialogContextType>({
|
||||
export const DialogContext = createContext<DialogContextType>({
|
||||
setIcon: () => null,
|
||||
setFooter: () => null,
|
||||
setIconPosition: () => null,
|
||||
});
|
||||
|
||||
export const DialogWrapperContext = React.createContext<DialogWrapperContextType>({
|
||||
export const DialogWrapperContext = createContext<DialogWrapperContextType>({
|
||||
props: {},
|
||||
setProps: () => null,
|
||||
close: () => null,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { IconPosition } from '@/components/elements/dialog/DialogIcon';
|
||||
|
||||
type Callback<T> = ((value: T) => void) | React.Dispatch<React.SetStateAction<T>>;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { ElementType, forwardRef, useMemo } from 'react';
|
||||
import { ElementType, forwardRef, useMemo } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import styles from './style.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
@ -23,8 +24,8 @@ const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
|
|||
const list = React.Children.toArray(children) as unknown as TypedChild[];
|
||||
|
||||
return [
|
||||
list.filter((child) => child.type === DropdownButton),
|
||||
list.filter((child) => child.type !== DropdownButton),
|
||||
list.filter(child => child.type === DropdownButton),
|
||||
list.filter(child => child.type !== DropdownButton),
|
||||
];
|
||||
}, [children]);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|||
import styles from '@/components/elements/dropdown/style.module.css';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import styles from './style.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
@ -26,7 +27,7 @@ const DropdownItem = forwardRef<HTMLAnchorElement, Props>(
|
|||
[styles.danger]: danger,
|
||||
[styles.disabled]: disabled,
|
||||
},
|
||||
className
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
@ -36,7 +37,7 @@ const DropdownItem = forwardRef<HTMLAnchorElement, Props>(
|
|||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default DropdownItem;
|
||||
|
|
206
resources/scripts/components/elements/editor/Editor.tsx
Normal file
206
resources/scripts/components/elements/editor/Editor.tsx
Normal file
|
@ -0,0 +1,206 @@
|
|||
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
indentOnInput,
|
||||
bracketMatching,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
LanguageDescription,
|
||||
indentUnit,
|
||||
} from '@codemirror/language';
|
||||
import { languages } from '@codemirror/language-data';
|
||||
import { lintKeymap } from '@codemirror/lint';
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { EditorState, Compartment } from '@codemirror/state';
|
||||
import {
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
EditorView,
|
||||
} from '@codemirror/view';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { ayuMirageHighlightStyle, ayuMirageTheme } from './theme';
|
||||
|
||||
function findLanguageByFilename(filename: string): LanguageDescription | undefined {
|
||||
const language = LanguageDescription.matchFilename(languages, filename);
|
||||
if (language !== null) {
|
||||
return language;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaultExtensions: Extension = [
|
||||
// Ayu Mirage
|
||||
ayuMirageTheme,
|
||||
syntaxHighlighting(ayuMirageHighlightStyle),
|
||||
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap,
|
||||
indentWithTab,
|
||||
]),
|
||||
EditorState.tabSize.of(4),
|
||||
indentUnit.of('\t'),
|
||||
];
|
||||
|
||||
export interface EditorProps {
|
||||
// DOM
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
|
||||
// CodeMirror Config
|
||||
extensions?: Extension[];
|
||||
language?: LanguageDescription;
|
||||
|
||||
// Options
|
||||
filename?: string;
|
||||
initialContent?: string;
|
||||
|
||||
// ?
|
||||
fetchContent?: (callback: () => Promise<string>) => void;
|
||||
|
||||
// Events
|
||||
onContentSaved?: () => void;
|
||||
onLanguageChanged?: (language: LanguageDescription | undefined) => void;
|
||||
}
|
||||
|
||||
export function Editor(props: EditorProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [view, setView] = useState<EditorView>();
|
||||
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [languageConfig] = useState(new Compartment());
|
||||
// eslint-disable-next-line react/hook-use-state
|
||||
const [keybindings] = useState(new Compartment());
|
||||
|
||||
const createEditorState = () =>
|
||||
EditorState.create({
|
||||
doc: props.initialContent,
|
||||
extensions: [
|
||||
defaultExtensions,
|
||||
props.extensions === undefined ? [] : props.extensions,
|
||||
languageConfig.of([]),
|
||||
keybindings.of([]),
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setView(
|
||||
new EditorView({
|
||||
state: createEditorState(),
|
||||
parent: ref.current,
|
||||
}),
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (view === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.destroy();
|
||||
setView(undefined);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.setState(createEditorState());
|
||||
}, [props.initialContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const language = props.language ?? findLanguageByFilename(props.filename ?? '');
|
||||
if (language === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
void language.load().then(language => {
|
||||
view.dispatch({
|
||||
effects: languageConfig.reconfigure(language),
|
||||
});
|
||||
});
|
||||
|
||||
if (props.onLanguageChanged !== undefined) {
|
||||
props.onLanguageChanged(language);
|
||||
}
|
||||
}, [view, props.filename, props.language]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.fetchContent === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!view) {
|
||||
props.fetchContent(async () => {
|
||||
throw new Error('no editor session has been configured');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { onContentSaved } = props;
|
||||
if (onContentSaved !== undefined) {
|
||||
view.dispatch({
|
||||
effects: keybindings.reconfigure(
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Mod-s',
|
||||
run() {
|
||||
onContentSaved();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
props.fetchContent(async () => view.state.doc.toJSON().join('\n'));
|
||||
}, [view, props.fetchContent, props.onContentSaved]);
|
||||
|
||||
return <div ref={ref} className={`relative ${props.className ?? ''}`.trimEnd()} style={props.style} />;
|
||||
}
|
1
resources/scripts/components/elements/editor/index.ts
Normal file
1
resources/scripts/components/elements/editor/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { Editor } from './Editor';
|
148
resources/scripts/components/elements/editor/theme.ts
Normal file
148
resources/scripts/components/elements/editor/theme.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
import { HighlightStyle } from '@codemirror/language';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
|
||||
const highlightBackground = 'transparent';
|
||||
const background = '#1F2430';
|
||||
const selection = '#34455A';
|
||||
const cursor = '#FFCC66';
|
||||
|
||||
export const ayuMirageTheme: Extension = EditorView.theme(
|
||||
{
|
||||
'&': {
|
||||
color: '#CBCCC6',
|
||||
backgroundColor: background,
|
||||
},
|
||||
|
||||
'.cm-content': {
|
||||
caretColor: cursor,
|
||||
},
|
||||
|
||||
'&.cm-focused .cm-cursor': { borderLeftColor: cursor },
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
'.cm-panels': { backgroundColor: '#232834', color: '#CBCCC6' },
|
||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: '#72a1ff59',
|
||||
outline: '1px solid #457dff',
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: '#6199ff2f',
|
||||
},
|
||||
|
||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||
|
||||
'.cm-matchingBracket, .cm-nonmatchingBracket': {
|
||||
backgroundColor: '#bad0f847',
|
||||
outline: '1px solid #515a6b',
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'transparent',
|
||||
color: '#FF3333',
|
||||
border: 'none',
|
||||
},
|
||||
|
||||
'.cm-gutterElement': {
|
||||
color: 'rgba(61, 66, 77, 99)',
|
||||
},
|
||||
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: highlightBackground,
|
||||
},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#ddd',
|
||||
},
|
||||
|
||||
'.cm-tooltip': {
|
||||
border: '1px solid #181a1f',
|
||||
backgroundColor: '#232834',
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: highlightBackground,
|
||||
color: '#CBCCC6',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: true },
|
||||
);
|
||||
|
||||
export const ayuMirageHighlightStyle = HighlightStyle.define([
|
||||
{
|
||||
tag: t.keyword,
|
||||
color: '#FFA759',
|
||||
},
|
||||
{
|
||||
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
|
||||
color: '#5CCFE6',
|
||||
},
|
||||
{
|
||||
tag: [t.function(t.variableName), t.labelName],
|
||||
color: '#CBCCC6',
|
||||
},
|
||||
{
|
||||
tag: [t.color, t.constant(t.name), t.standard(t.name)],
|
||||
color: '#F29E74',
|
||||
},
|
||||
{
|
||||
tag: [t.definition(t.name), t.separator],
|
||||
color: '#CBCCC6B3',
|
||||
},
|
||||
{
|
||||
tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
|
||||
color: '#FFCC66',
|
||||
},
|
||||
{
|
||||
tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)],
|
||||
color: '#5CCFE6',
|
||||
},
|
||||
{
|
||||
tag: [t.meta, t.comment],
|
||||
color: '#5C6773',
|
||||
},
|
||||
{
|
||||
tag: t.strong,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
{
|
||||
tag: t.emphasis,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
{
|
||||
tag: t.strikethrough,
|
||||
textDecoration: 'line-through',
|
||||
},
|
||||
{
|
||||
tag: t.link,
|
||||
color: '#FF3333',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
{
|
||||
tag: t.heading,
|
||||
fontWeight: 'bold',
|
||||
color: '#BAE67E',
|
||||
},
|
||||
{
|
||||
tag: [t.atom, t.bool, t.special(t.variableName)],
|
||||
color: '#5CCFE6',
|
||||
},
|
||||
{
|
||||
tag: [t.processingInstruction, t.string, t.inserted],
|
||||
color: '#BAE67E',
|
||||
},
|
||||
{
|
||||
tag: t.invalid,
|
||||
color: '#FF3333',
|
||||
},
|
||||
]);
|
|
@ -1,4 +1,5 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
|
@ -19,7 +20,7 @@ const Component = forwardRef<HTMLInputElement, InputFieldProps>(({ className, va
|
|||
'form-input',
|
||||
styles.text_input,
|
||||
{ [styles.loose]: variant === Variant.Loose },
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
@ -6,7 +6,7 @@ const Input = Object.assign(
|
|||
{
|
||||
Text: InputField,
|
||||
Checkbox: Checkbox,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export { Input };
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { PaginationDataSet } from '@/api/http';
|
||||
import classNames from 'classnames';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
|
@ -53,7 +52,7 @@ const PaginationFooter = ({ pagination, className, onPageSelect }: Props) => {
|
|||
<Button.Text {...buttonProps(1)} disabled={pages.previous.length !== 2}>
|
||||
<ChevronDoubleLeftIcon className={'w-3 h-3'} />
|
||||
</Button.Text>
|
||||
{pages.previous.reverse().map((value) => (
|
||||
{pages.previous.reverse().map(value => (
|
||||
<Button.Text key={`previous-${value}`} {...buttonProps(value)}>
|
||||
{value}
|
||||
</Button.Text>
|
||||
|
@ -61,7 +60,7 @@ const PaginationFooter = ({ pagination, className, onPageSelect }: Props) => {
|
|||
<Button size={Button.Sizes.Small} shape={Button.Shapes.IconSquare}>
|
||||
{current}
|
||||
</Button>
|
||||
{pages.next.map((value) => (
|
||||
{pages.next.map(value => (
|
||||
<Button.Text key={`next-${value}`} {...buttonProps(value)}>
|
||||
{value}
|
||||
</Button.Text>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { cloneElement, useRef, useState } from 'react';
|
||||
import { cloneElement, useRef, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
arrow,
|
||||
autoUpdate,
|
||||
|
@ -104,7 +105,7 @@ export default ({ children, ...props }: Props) => {
|
|||
ref={arrowEl}
|
||||
style={{
|
||||
transform: `translate(${Math.round(ax || 0)}px, ${Math.round(
|
||||
ay || 0
|
||||
ay || 0,
|
||||
)}px) rotate(45deg)`,
|
||||
}}
|
||||
className={classNames('absolute bg-gray-900 w-3 h-3', side)}
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import React from 'react';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import type { ElementType, ReactNode } from 'react';
|
||||
|
||||
type Duration = `duration-${number}`;
|
||||
|
||||
interface Props {
|
||||
as?: React.ElementType;
|
||||
as?: ElementType;
|
||||
duration?: Duration | [Duration, Duration];
|
||||
appear?: boolean;
|
||||
unmount?: boolean;
|
||||
show: boolean;
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default ({ children, duration, ...props }: Props) => {
|
||||
function FadeTransition({ children, duration, ...props }: Props) {
|
||||
const [enterDuration, exitDuration] = Array.isArray(duration)
|
||||
? duration
|
||||
: !duration
|
||||
|
@ -30,4 +32,6 @@ export default ({ children, duration, ...props }: Props) => {
|
|||
{children}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default FadeTransition;
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import { createBrowserHistory } from 'history';
|
||||
|
||||
export const history = createBrowserHistory({ basename: '/' });
|
|
@ -1,15 +1,14 @@
|
|||
import React from 'react';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import ScreenBlock from '@/components/elements/ScreenBlock';
|
||||
import ServerInstallSvg from '@/assets/images/server_installing.svg';
|
||||
import ServerErrorSvg from '@/assets/images/server_error.svg';
|
||||
import ServerRestoreSvg from '@/assets/images/server_restore.svg';
|
||||
import ScreenBlock from '@/components/elements/ScreenBlock';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
export default () => {
|
||||
const status = ServerContext.useStoreState((state) => state.server.data?.status || null);
|
||||
const isTransferring = ServerContext.useStoreState((state) => state.server.data?.isTransferring || false);
|
||||
const status = ServerContext.useStoreState(state => state.server.data?.status || null);
|
||||
const isTransferring = ServerContext.useStoreState(state => state.server.data?.isTransferring || false);
|
||||
const isNodeUnderMaintenance = ServerContext.useStoreState(
|
||||
(state) => state.server.data?.isNodeUnderMaintenance || false
|
||||
state => state.server.data?.isNodeUnderMaintenance || false,
|
||||
);
|
||||
|
||||
return status === 'installing' || status === 'install_failed' ? (
|
||||
|
@ -36,7 +35,7 @@ export default () => {
|
|||
image={ServerRestoreSvg}
|
||||
message={
|
||||
isTransferring
|
||||
? 'Your server is being transfered to a new node, please check back later.'
|
||||
? 'Your server is being transferred to a new node, please check back later.'
|
||||
: 'Your server is currently being restored from a backup, please check back in a few minutes.'
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -5,26 +5,26 @@ import { mutate } from 'swr';
|
|||
import { getDirectorySwrKey } from '@/plugins/useFileManagerSwr';
|
||||
|
||||
const InstallListener = () => {
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const getServer = ServerContext.useStoreActions((actions) => actions.server.getServer);
|
||||
const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||
|
||||
useWebsocketEvent(SocketEvent.BACKUP_RESTORE_COMPLETED, () => {
|
||||
mutate(getDirectorySwrKey(uuid, '/'), undefined);
|
||||
setServerFromState((s) => ({ ...s, status: null }));
|
||||
setServerFromState(s => ({ ...s, status: null }));
|
||||
});
|
||||
|
||||
// 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(SocketEvent.INSTALL_COMPLETED, () => {
|
||||
getServer(uuid).catch((error) => console.error(error));
|
||||
getServer(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(SocketEvent.INSTALL_STARTED, () => {
|
||||
setServerFromState((s) => ({ ...s, status: 'installing' }));
|
||||
setServerFromState(s => ({ ...s, status: 'installing' }));
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useActivityLogs } from '@/api/server/activity';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
|
@ -24,7 +24,7 @@ export default () => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
setFilters((value) => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
|
||||
setFilters(value => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
|
||||
}, [hash]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -39,7 +39,7 @@ export default () => {
|
|||
<Link
|
||||
to={'#'}
|
||||
className={classNames(btnStyles.button, btnStyles.text, 'w-full sm:w-auto')}
|
||||
onClick={() => setFilters((value) => ({ ...value, filters: {} }))}
|
||||
onClick={() => setFilters(value => ({ ...value, filters: {} }))}
|
||||
>
|
||||
Clear Filters <XCircleIcon className={'w-4 h-4 ml-2'} />
|
||||
</Link>
|
||||
|
@ -51,7 +51,7 @@ export default () => {
|
|||
<p className={'text-sm text-center text-gray-400'}>No activity logs available for this server.</p>
|
||||
) : (
|
||||
<div className={'bg-gray-700'}>
|
||||
{data?.items.map((activity) => (
|
||||
{data?.items.map(activity => (
|
||||
<ActivityLogEntry key={activity.id} activity={activity}>
|
||||
<span />
|
||||
</ActivityLogEntry>
|
||||
|
@ -61,7 +61,7 @@ export default () => {
|
|||
{data && (
|
||||
<PaginationFooter
|
||||
pagination={data.pagination}
|
||||
onPageSelect={(page) => setFilters((value) => ({ ...value, page }))}
|
||||
onPageSelect={page => setFilters(value => ({ ...value, page }))}
|
||||
/>
|
||||
)}
|
||||
</ServerContentBlock>
|
||||
|
|
|
@ -3,19 +3,19 @@ import { ServerContext } from '@/state/server';
|
|||
import { SocketEvent } from '@/components/server/events';
|
||||
|
||||
const TransferListener = () => {
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const getServer = ServerContext.useStoreActions((actions) => actions.server.getServer);
|
||||
const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||
|
||||
// Listen for the transfer status event, so we can update the state of the server.
|
||||
useWebsocketEvent(SocketEvent.TRANSFER_STATUS, (status: string) => {
|
||||
if (status === 'pending' || status === 'processing') {
|
||||
setServerFromState((s) => ({ ...s, isTransferring: true }));
|
||||
setServerFromState(s => ({ ...s, isTransferring: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
setServerFromState((s) => ({ ...s, isTransferring: false }));
|
||||
setServerFromState(s => ({ ...s, isTransferring: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ const TransferListener = () => {
|
|||
}
|
||||
|
||||
// Refresh the server's information as it's node and allocations were just updated.
|
||||
getServer(uuid).catch((error) => console.error(error));
|
||||
getServer(uuid).catch(error => console.error(error));
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import React from 'react';
|
||||
|
||||
export default ({ uptime }: { uptime: number }) => {
|
||||
const days = Math.floor(uptime / (24 * 60 * 60));
|
||||
const hours = Math.floor((Math.floor(uptime) / 60 / 60) % 24);
|
||||
|
|
|
@ -1,29 +1,32 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Websocket } from '@/plugins/Websocket';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { useEffect, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
import getWebsocketToken from '@/api/server/getWebsocketToken';
|
||||
import ContentContainer from '@/components/elements/ContentContainer';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import tw from 'twin.macro';
|
||||
import FadeTransition from '@/components/elements/transitions/FadeTransition';
|
||||
import { Websocket } from '@/plugins/Websocket';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
const reconnectErrors = ['jwt: exp claim is invalid', 'jwt: created too far in past (denylist)'];
|
||||
|
||||
export default () => {
|
||||
function WebsocketHandler() {
|
||||
let updatingToken = false;
|
||||
const [error, setError] = useState<'connecting' | string>('');
|
||||
const { connected, instance } = ServerContext.useStoreState((state) => state.socket);
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
|
||||
const setServerStatus = ServerContext.useStoreActions((actions) => actions.status.setServerStatus);
|
||||
const { setInstance, setConnectionState } = ServerContext.useStoreActions((actions) => actions.socket);
|
||||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
|
||||
const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus);
|
||||
const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket);
|
||||
|
||||
const updateToken = (uuid: string, socket: Websocket) => {
|
||||
if (updatingToken) return;
|
||||
if (updatingToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatingToken = true;
|
||||
getWebsocketToken(uuid)
|
||||
.then((data) => socket.setToken(data.token, true))
|
||||
.catch((error) => console.error(error))
|
||||
.then(data => socket.setToken(data.token, true))
|
||||
.catch(error => console.error(error))
|
||||
.then(() => {
|
||||
updatingToken = false;
|
||||
});
|
||||
|
@ -38,9 +41,9 @@ export default () => {
|
|||
setError('connecting');
|
||||
setConnectionState(false);
|
||||
});
|
||||
socket.on('status', (status) => setServerStatus(status));
|
||||
socket.on('status', status => setServerStatus(status));
|
||||
|
||||
socket.on('daemon error', (message) => {
|
||||
socket.on('daemon error', message => {
|
||||
console.warn('Got error message from daemon socket:', message);
|
||||
});
|
||||
|
||||
|
@ -50,11 +53,11 @@ export default () => {
|
|||
setConnectionState(false);
|
||||
console.warn('JWT validation error from wings:', error);
|
||||
|
||||
if (reconnectErrors.find((v) => error.toLowerCase().indexOf(v) >= 0)) {
|
||||
if (reconnectErrors.find(v => error.toLowerCase().indexOf(v) >= 0)) {
|
||||
updateToken(uuid, socket);
|
||||
} else {
|
||||
setError(
|
||||
'There was an error validating the credentials provided for the websocket. Please refresh the page.'
|
||||
'There was an error validating the credentials provided for the websocket. Please refresh the page.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -74,14 +77,14 @@ export default () => {
|
|||
});
|
||||
|
||||
getWebsocketToken(uuid)
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
// Connect and then set the authentication token.
|
||||
socket.setToken(data.token).connect(data.socket);
|
||||
|
||||
// Once that is done, set the instance.
|
||||
setInstance(socket);
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
.catch(error => console.error(error));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -105,7 +108,7 @@ export default () => {
|
|||
}, [uuid]);
|
||||
|
||||
return error ? (
|
||||
<CSSTransition timeout={150} in appear classNames={'fade'}>
|
||||
<FadeTransition duration="duration-150" appear show>
|
||||
<div css={tw`bg-red-500 py-2`}>
|
||||
<ContentContainer css={tw`flex items-center justify-center`}>
|
||||
{error === 'connecting' ? (
|
||||
|
@ -120,6 +123,8 @@ export default () => {
|
|||
)}
|
||||
</ContentContainer>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</FadeTransition>
|
||||
) : null;
|
||||
};
|
||||
}
|
||||
|
||||
export default WebsocketHandler;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import Can from '@/components/elements/Can';
|
||||
|
@ -16,7 +16,7 @@ const BackupContainer = () => {
|
|||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { data: backups, error, isValidating } = getServerBackups();
|
||||
|
||||
const backupLimit = ServerContext.useStoreState((state) => state.server.data!.featureLimits.backups);
|
||||
const backupLimit = ServerContext.useStoreState(state => state.server.data!.featureLimits.backups);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
faBoxOpen,
|
||||
faCloudDownloadAlt,
|
||||
|
@ -28,8 +28,8 @@ interface Props {
|
|||
}
|
||||
|
||||
export default ({ backup }: Props) => {
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||
const [modal, setModal] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [truncate, setTruncate] = useState(false);
|
||||
|
@ -40,11 +40,11 @@ export default ({ backup }: Props) => {
|
|||
setLoading(true);
|
||||
clearFlashes('backups');
|
||||
getBackupDownloadUrl(uuid, backup.uuid)
|
||||
.then((url) => {
|
||||
.then(url => {
|
||||
// @ts-expect-error this is valid
|
||||
window.location = url;
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'backups', error });
|
||||
})
|
||||
|
@ -55,17 +55,18 @@ export default ({ backup }: Props) => {
|
|||
setLoading(true);
|
||||
clearFlashes('backups');
|
||||
deleteBackup(uuid, backup.uuid)
|
||||
.then(() =>
|
||||
mutate(
|
||||
(data) => ({
|
||||
...data,
|
||||
items: data.items.filter((b) => b.uuid !== backup.uuid),
|
||||
backupCount: data.backupCount - 1,
|
||||
}),
|
||||
false
|
||||
)
|
||||
.then(
|
||||
async () =>
|
||||
await mutate(
|
||||
data => ({
|
||||
...data!,
|
||||
items: data!.items.filter(b => b.uuid !== backup.uuid),
|
||||
backupCount: data!.backupCount - 1,
|
||||
}),
|
||||
false,
|
||||
),
|
||||
)
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'backups', error });
|
||||
setLoading(false);
|
||||
|
@ -78,12 +79,12 @@ export default ({ backup }: Props) => {
|
|||
clearFlashes('backups');
|
||||
restoreServerBackup(uuid, backup.uuid, truncate)
|
||||
.then(() =>
|
||||
setServerFromState((s) => ({
|
||||
setServerFromState(s => ({
|
||||
...s,
|
||||
status: 'restoring_backup',
|
||||
}))
|
||||
})),
|
||||
)
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'backups', error });
|
||||
})
|
||||
|
@ -97,23 +98,24 @@ export default ({ backup }: Props) => {
|
|||
}
|
||||
|
||||
http.post(`/api/client/servers/${uuid}/backups/${backup.uuid}/lock`)
|
||||
.then(() =>
|
||||
mutate(
|
||||
(data) => ({
|
||||
...data,
|
||||
items: data.items.map((b) =>
|
||||
b.uuid !== backup.uuid
|
||||
? b
|
||||
: {
|
||||
...b,
|
||||
isLocked: !b.isLocked,
|
||||
}
|
||||
),
|
||||
}),
|
||||
false
|
||||
)
|
||||
.then(
|
||||
async () =>
|
||||
await mutate(
|
||||
data => ({
|
||||
...data!,
|
||||
items: data!.items.map(b =>
|
||||
b.uuid !== backup.uuid
|
||||
? b
|
||||
: {
|
||||
...b,
|
||||
isLocked: !b.isLocked,
|
||||
},
|
||||
),
|
||||
}),
|
||||
false,
|
||||
),
|
||||
)
|
||||
.catch((error) => alert(httpErrorToHuman(error)))
|
||||
.catch(error => alert(httpErrorToHuman(error)))
|
||||
.then(() => setModal(''));
|
||||
};
|
||||
|
||||
|
@ -146,7 +148,7 @@ export default ({ backup }: Props) => {
|
|||
id={'restore_truncate'}
|
||||
value={'true'}
|
||||
checked={truncate}
|
||||
onChange={() => setTruncate((s) => !s)}
|
||||
onChange={() => setTruncate(s => !s)}
|
||||
/>
|
||||
Delete all files before restoring backup.
|
||||
</label>
|
||||
|
@ -164,7 +166,7 @@ export default ({ backup }: Props) => {
|
|||
<SpinnerOverlay visible={loading} fixed />
|
||||
{backup.isSuccessful ? (
|
||||
<DropdownMenu
|
||||
renderToggle={(onClick) => (
|
||||
renderToggle={onClick => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
css={tw`text-gray-200 transition-colors duration-150 hover:text-gray-100 p-2`}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faArchive, faEllipsisH, faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
|
@ -21,14 +20,14 @@ interface Props {
|
|||
export default ({ backup, className }: Props) => {
|
||||
const { mutate } = getServerBackups();
|
||||
|
||||
useWebsocketEvent(`${SocketEvent.BACKUP_COMPLETED}:${backup.uuid}` as SocketEvent, (data) => {
|
||||
useWebsocketEvent(`${SocketEvent.BACKUP_COMPLETED}:${backup.uuid}` as SocketEvent, async data => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
mutate(
|
||||
(data) => ({
|
||||
...data,
|
||||
items: data.items.map((b) =>
|
||||
await mutate(
|
||||
data => ({
|
||||
...data!,
|
||||
items: data!.items.map(b =>
|
||||
b.uuid !== backup.uuid
|
||||
? b
|
||||
: {
|
||||
|
@ -37,10 +36,10 @@ export default ({ backup, className }: Props) => {
|
|||
checksum: (parsed.checksum_type || '') + ':' + (parsed.checksum || ''),
|
||||
bytes: parsed.file_size || 0,
|
||||
completedAt: new Date(),
|
||||
}
|
||||
},
|
||||
),
|
||||
}),
|
||||
false
|
||||
false,
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||
import { Field as FormikField, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { boolean, object, string } from 'yup';
|
||||
|
@ -68,7 +68,7 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
|
|||
};
|
||||
|
||||
export default () => {
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { mutate } = getServerBackups();
|
||||
|
@ -80,14 +80,14 @@ export default () => {
|
|||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('backups:create');
|
||||
createServerBackup(uuid, values)
|
||||
.then((backup) => {
|
||||
mutate(
|
||||
(data) => ({ ...data, items: data.items.concat(backup), backupCount: data.backupCount + 1 }),
|
||||
false
|
||||
.then(async backup => {
|
||||
await mutate(
|
||||
data => ({ ...data!, items: data!.items.concat(backup), backupCount: data!.backupCount + 1 }),
|
||||
false,
|
||||
);
|
||||
setVisible(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
clearAndAddHttpError({ key: 'backups:create', error });
|
||||
setSubmitting(false);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from '@/components/server/console/style.module.css';
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue