React 18 and Vite (#4510)

This commit is contained in:
Matthew Penner 2022-11-25 13:25:03 -07:00 committed by GitHub
parent 1bb1b13f6d
commit 21613fa602
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
244 changed files with 4547 additions and 8933 deletions

View file

@ -1,30 +0,0 @@
import React from 'react';
import { Route } from 'react-router';
import { SwitchTransition } from 'react-transition-group';
import Fade from '@/components/elements/Fade';
import styled from 'styled-components/macro';
import tw from 'twin.macro';
const StyledSwitchTransition = styled(SwitchTransition)`
${tw`relative`};
& section {
${tw`absolute w-full top-0 left-0`};
}
`;
const TransitionRouter: React.FC = ({ children }) => {
return (
<Route
render={({ location }) => (
<StyledSwitchTransition>
<Fade timeout={150} key={location.pathname + location.search} in appear unmountOnExit>
<section>{children}</section>
</Fade>
</StyledSwitchTransition>
)}
/>
);
};
export default TransitionRouter;

View file

@ -1 +0,0 @@
module.exports = 'test-file-stub';

View file

@ -1,8 +1,10 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr';
import { ActivityLog, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import type { AxiosError } from 'axios';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import { toPaginatedSet } from '@definitions/helpers';
import { ActivityLog, Transformers } from '@definitions/user';
import useFilteredObject from '@/plugins/useFilteredObject';
import { useUserSWRKey } from '@/plugins/useSWRKey';
@ -10,8 +12,8 @@ export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>
const useActivityLogs = (
filters?: ActivityLogFilters,
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
config?: SWRConfiguration<PaginatedResult<ActivityLog>, AxiosError>,
) => {
const key = useUserSWRKey(['account', 'activity', JSON.stringify(useFilteredObject(filters || {}))]);
return useSWR<PaginatedResult<ActivityLog>>(
@ -26,7 +28,7 @@ const useActivityLogs = (
return toPaginatedSet(data, Transformers.toActivityLog);
},
{ revalidateOnMount: false, ...(config || {}) }
{ revalidateOnMount: false, ...(config || {}) },
);
};

View file

@ -12,7 +12,7 @@ export default (description: string, allowedIps: string): Promise<ApiKey & { sec
...rawDataToApiKey(data.attributes),
// eslint-disable-next-line camelcase
secretToken: data.meta?.secret_token ?? '',
})
}),
)
.catch(reject);
});

View file

@ -1,10 +1,12 @@
import useSWR, { ConfigInterface } from 'swr';
import type { AxiosError } from 'axios';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
import http, { FractalResponseList } from '@/api/http';
import { SSHKey, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import { useUserSWRKey } from '@/plugins/useSWRKey';
const useSSHKeys = (config?: ConfigInterface<SSHKey[], AxiosError>) => {
const useSSHKeys = (config?: SWRConfiguration<SSHKey[], AxiosError>) => {
const key = useUserSWRKey(['account', 'ssh-keys']);
return useSWR(
@ -16,7 +18,7 @@ const useSSHKeys = (config?: ConfigInterface<SSHKey[], AxiosError>) => {
return Transformers.toSSHKey(datum.attributes);
});
},
{ revalidateOnMount: false, ...(config || {}) }
{ revalidateOnMount: false, ...(config || {}) },
);
};

View file

@ -20,9 +20,9 @@ export default ({ username, password, recaptchaData }: LoginData): Promise<Login
user: username,
password,
'g-recaptcha-response': recaptchaData,
})
}),
)
.then((response) => {
.then(response => {
if (!(response.data instanceof Object)) {
return reject(new Error('An error occurred while processing the login request.'));
}

View file

@ -8,11 +8,11 @@ export default (token: string, code: string, recoveryToken?: string): Promise<Lo
authentication_code: code,
recovery_token: recoveryToken && recoveryToken.length > 0 ? recoveryToken : undefined,
})
.then((response) =>
.then(response =>
resolve({
complete: response.data.data.complete,
intended: response.data.data.intended || undefined,
})
}),
)
.catch(reject);
});

View file

@ -19,11 +19,11 @@ export default (email: string, data: Data): Promise<PasswordResetResponse> => {
password: data.password,
password_confirmation: data.passwordConfirmation,
})
.then((response) =>
.then(response =>
resolve({
redirectTo: response.data.redirect_to,
sendToLogin: response.data.send_to_login,
})
}),
)
.catch(reject);
});

View file

@ -3,7 +3,7 @@ import http from '@/api/http';
export default (email: string, recaptchaData?: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.post('/auth/password', { email, 'g-recaptcha-response': recaptchaData })
.then((response) => resolve(response.data.status || ''))
.then(response => resolve(response.data.status || ''))
.catch(reject);
});
};

View file

@ -15,17 +15,17 @@ function transform<T, M>(data: null | undefined, transformer: TransformerFunc<T>
function transform<T, M>(
data: FractalResponseData | null | undefined,
transformer: TransformerFunc<T>,
missing?: M
missing?: M,
): T | M;
function transform<T, M>(
data: FractalResponseList | FractalPaginatedResponse | null | undefined,
transformer: TransformerFunc<T>,
missing?: M
missing?: M,
): T[] | M;
function transform<T>(
data: FractalResponseData | FractalResponseList | FractalPaginatedResponse | null | undefined,
transformer: TransformerFunc<T>,
missing = undefined
missing = undefined,
) {
if (data === undefined || data === null) {
return missing;
@ -44,7 +44,7 @@ function transform<T>(
function toPaginatedSet<T extends TransformerFunc<Model>>(
response: FractalPaginatedResponse,
transformer: T
transformer: T,
): PaginatedResult<ReturnType<T>> {
return {
items: transform(response, transformer) as ReturnType<T>[],

View file

@ -19,7 +19,7 @@ export default ({ query, ...params }: QueryParams): Promise<PaginatedResult<Serv
resolve({
items: (data.data || []).map((datum: any) => rawDataToServerObject(datum)),
pagination: getPaginationSet(data.meta.pagination),
})
}),
)
.catch(reject);
});

View file

@ -11,7 +11,7 @@ const http: AxiosInstance = axios.create({
},
});
http.interceptors.request.use((req) => {
http.interceptors.request.use(req => {
if (!req.url?.endsWith('/resources')) {
store.getActions().progress.startContinuous();
}
@ -20,18 +20,18 @@ http.interceptors.request.use((req) => {
});
http.interceptors.response.use(
(resp) => {
resp => {
if (!resp.request?.url?.endsWith('/resources')) {
store.getActions().progress.setComplete();
}
return resp;
},
(error) => {
error => {
store.getActions().progress.setComplete();
throw error;
}
},
);
export default http;

View file

@ -1,21 +1,22 @@
import http from '@/api/http';
import { AxiosError } from 'axios';
import { History } from 'history';
import type { AxiosError } from 'axios';
import type { NavigateFunction } from 'react-router-dom';
export const setupInterceptors = (history: History) => {
import http from '@/api/http';
export const setupInterceptors = (navigate: NavigateFunction) => {
http.interceptors.response.use(
(resp) => resp,
resp => resp,
(error: AxiosError) => {
if (error.response?.status === 400) {
if (
(error.response?.data as Record<string, any>).errors?.[0].code === 'TwoFactorAuthRequiredException'
) {
if (!window.location.pathname.startsWith('/account')) {
history.replace('/account', { twoFactorRedirect: true });
navigate('/account', { state: { twoFactorRedirect: true } });
}
}
}
throw error;
}
},
);
};

View file

@ -1,8 +1,12 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr';
import { ActivityLog, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import http, { PaginatedResult, QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import type { AxiosError } from 'axios';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
import type { PaginatedResult, QueryBuilderParams } from '@/api/http';
import http, { withQueryBuilderParams } from '@/api/http';
import { toPaginatedSet } from '@definitions/helpers';
import type { ActivityLog } from '@definitions/user';
import { Transformers } from '@definitions/user';
import useFilteredObject from '@/plugins/useFilteredObject';
import { useServerSWRKey } from '@/plugins/useSWRKey';
import { ServerContext } from '@/state/server';
@ -11,9 +15,9 @@ export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>
const useActivityLogs = (
filters?: ActivityLogFilters,
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
config?: SWRConfiguration<PaginatedResult<ActivityLog>, AxiosError>,
) => {
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
const key = useServerSWRKey(['activity', useFilteredObject(filters || {})]);
return useSWR<PaginatedResult<ActivityLog>>(
@ -28,7 +32,7 @@ const useActivityLogs = (
return toPaginatedSet(data, Transformers.toActivityLog);
},
{ revalidateOnMount: false, ...(config || {}) }
{ revalidateOnMount: false, ...(config || {}) },
);
};

View file

@ -11,9 +11,9 @@ export default (uuid: string, data: { connectionsFrom: string; databaseName: str
},
{
params: { include: 'password' },
}
},
)
.then((response) => resolve(rawDataToServerDatabase(response.data.attributes)))
.then(response => resolve(rawDataToServerDatabase(response.data.attributes)))
.catch(reject);
});
};

View file

@ -23,8 +23,8 @@ export default (uuid: string, includePassword = true): Promise<ServerDatabase[]>
http.get(`/api/client/servers/${uuid}/databases`, {
params: includePassword ? { include: 'password' } : undefined,
})
.then((response) =>
resolve((response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes)))
.then(response =>
resolve((response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes))),
)
.catch(reject);
});

View file

@ -4,7 +4,7 @@ import http from '@/api/http';
export default (uuid: string, database: string): Promise<ServerDatabase> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/databases/${database}/rotate-password`)
.then((response) => resolve(rawDataToServerDatabase(response.data.attributes)))
.then(response => resolve(rawDataToServerDatabase(response.data.attributes)))
.catch(reject);
});
};

View file

@ -10,7 +10,7 @@ export default async (uuid: string, directory: string, files: string[]): Promise
timeout: 60000,
timeoutErrorMessage:
'It looks like this archive is taking a long time to generate. It will appear once completed.',
}
},
);
return rawDataToFileObject(data);

View file

@ -8,6 +8,6 @@ export default async (uuid: string, directory: string, file: string): Promise<vo
timeout: 300000,
timeoutErrorMessage:
'It looks like this archive is taking a long time to be unarchived. Once completed the unarchived files will appear.',
}
},
);
};

View file

@ -4,7 +4,7 @@ export default (server: string, file: string): Promise<string> => {
return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${server}/files/contents`, {
params: { file },
transformResponse: (res) => res,
transformResponse: res => res,
responseType: 'text',
})
.then(({ data }) => resolve(data))

View file

@ -25,7 +25,7 @@ export interface Server {
};
invocation: string;
dockerImage: string;
description: string;
description: string | null;
limits: {
memory: number;
swap: number;
@ -65,10 +65,10 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
featureLimits: { ...data.feature_limits },
isTransferring: data.is_transferring,
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(
rawDataToServerEggVariable
rawDataToServerEggVariable,
),
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(
rawDataToServerAllocation
rawDataToServerAllocation,
),
});
@ -80,7 +80,7 @@ export default (uuid: string): Promise<[Server, string[]]> => {
rawDataToServerObject(data),
// eslint-disable-next-line camelcase
data.meta?.is_server_owner ? ['*'] : data.meta?.user_permissions || [],
])
]),
)
.catch(reject);
});

View file

@ -26,7 +26,7 @@ export default (server: string): Promise<ServerStats> => {
networkRxInBytes: attributes.resources.network_rx_bytes,
networkTxInBytes: attributes.resources.network_tx_bytes,
uptime: attributes.resources.uptime,
})
}),
)
.catch(reject);
});

View file

@ -12,7 +12,7 @@ export default (server: string): Promise<Response> => {
resolve({
token: data.data.token,
socket: data.data.socket,
})
}),
)
.catch(reject);
});

View file

@ -16,7 +16,7 @@ export default async (uuid: string, schedule: number, task: number | undefined,
payload: data.payload,
continue_on_failure: data.continueOnFailure,
time_offset: data.timeOffset,
}
},
);
return rawDataToServerTask(response.attributes);

View file

@ -12,7 +12,7 @@ export default (uuid: string, params: Params, subuser?: Subuser): Promise<Subuse
http.post(`/api/client/servers/${uuid}/users${subuser ? `/${subuser.uuid}` : ''}`, {
...params,
})
.then((data) => resolve(rawDataToServerSubuser(data.data)))
.then(data => resolve(rawDataToServerSubuser(data.data)))
.catch(reject);
});
};

View file

@ -9,7 +9,7 @@ export const rawDataToServerSubuser = (data: FractalResponseData): Subuser => ({
twoFactorEnabled: data.attributes['2fa_enabled'],
createdAt: new Date(data.attributes.created_at),
permissions: data.attributes.permissions || [],
can: (permission) => (data.attributes.permissions || []).indexOf(permission) >= 0,
can: permission => (data.attributes.permissions || []).indexOf(permission) >= 0,
});
export default (uuid: string): Promise<Subuser[]> => {

View file

@ -1,11 +1,12 @@
import { ServerContext } from '@/state/server';
import useSWR from 'swr';
import http from '@/api/http';
import { rawDataToServerAllocation } from '@/api/transformers';
import { Allocation } from '@/api/server/getServer';
import { rawDataToServerAllocation } from '@/api/transformers';
import { ServerContext } from '@/state/server';
export default () => {
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
return useSWR<Allocation[]>(
['server:allocations', uuid],
@ -14,6 +15,6 @@ export default () => {
return (data.data || []).map(rawDataToServerAllocation);
},
{ revalidateOnFocus: false, revalidateOnMount: false }
{ revalidateOnFocus: false, revalidateOnMount: false },
);
};

View file

@ -1,9 +1,11 @@
import { createContext, useContext } from 'react';
import useSWR from 'swr';
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
import { ServerBackup } from '@/api/server/types';
import type { PaginatedResult } from '@/api/http';
import http, { getPaginationSet } from '@/api/http';
import type { ServerBackup } from '@/api/server/types';
import { rawDataToServerBackup } from '@/api/transformers';
import { ServerContext } from '@/state/server';
import { createContext, useContext } from 'react';
interface ctx {
page: number;
@ -16,7 +18,7 @@ type BackupResponse = PaginatedResult<ServerBackup> & { backupCount: number };
export default () => {
const { page } = useContext(Context);
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
return useSWR<BackupResponse>(['server:backups', uuid, page], async () => {
const { data } = await http.get(`/api/client/servers/${uuid}/backups`, { params: { page } });

View file

@ -1,7 +1,10 @@
import useSWR, { ConfigInterface } from 'swr';
import type { AxiosError } from 'axios';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
import http, { FractalResponseList } from '@/api/http';
import type { ServerEggVariable } from '@/api/server/types';
import { rawDataToServerEggVariable } from '@/api/transformers';
import { ServerEggVariable } from '@/api/server/types';
interface Response {
invocation: string;
@ -9,7 +12,7 @@ interface Response {
dockerImages: Record<string, string>;
}
export default (uuid: string, initialData?: Response | null, config?: ConfigInterface<Response>) =>
export default (uuid: string, fallbackData?: Response, config?: SWRConfiguration<Response, AxiosError>) =>
useSWR(
[uuid, '/startup'],
async (): Promise<Response> => {
@ -23,5 +26,5 @@ export default (uuid: string, initialData?: Response | null, config?: ConfigInte
dockerImages: data.meta.docker_images || {},
};
},
{ initialData: initialData || undefined, errorRetryCount: 3, ...(config || {}) }
{ fallbackData, errorRetryCount: 3, ...(config ?? {}) },
);

View file

@ -49,7 +49,7 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
const matches = ['application/jar', 'application/octet-stream', 'inode/directory', /^image\//];
return matches.every((m) => !this.mimetype.match(m));
return matches.every(m => !this.mimetype.match(m));
},
});

View file

@ -1,5 +1,5 @@
import tw from 'twin.macro';
import { createGlobalStyle } from 'styled-components/macro';
import { createGlobalStyle } from 'styled-components';
export default createGlobalStyle`
body {

View file

@ -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 };

View file

@ -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} />;
};

View file

@ -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;

View file

@ -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';

View file

@ -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>

View file

@ -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;

View file

@ -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} />;
};

View file

@ -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;

View file

@ -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';

View file

@ -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;

View file

@ -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} />

View file

@ -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>

View file

@ -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';

View file

@ -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>
)}

View file

@ -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>

View file

@ -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>

View file

@ -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';

View file

@ -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) });

View file

@ -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>

View file

@ -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'}>&nbsp;</span>

View file

@ -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>

View file

@ -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>
);

View file

@ -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>
);
};

View file

@ -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} />

View file

@ -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>

View file

@ -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';

View file

@ -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));
};

View file

@ -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);
});

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import classNames from 'classnames';
interface CodeProps {

View file

@ -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,

View file

@ -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);

View file

@ -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';

View file

@ -1,4 +1,4 @@
import styled from 'styled-components/macro';
import styled from 'styled-components';
import { breakpoint } from '@/theme';
import tw from 'twin.macro';

View file

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

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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`};

View file

@ -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';

View file

@ -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>`

View file

@ -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}</>

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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&reg;
</a>
&nbsp;&copy; 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&reg;
</a>
&nbsp;&copy; 2015 - {new Date().getFullYear()}
</p>
</ContentContainer>
</>
);
};
}
export default PageContentBlock;

View file

@ -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>

View file

@ -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;

View file

@ -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 }) => {

View file

@ -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;

View file

@ -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';

View file

@ -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`};

View file

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

View file

@ -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 }) =>

View file

@ -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;

View file

@ -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`

View file

@ -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}
/>

View file

@ -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';

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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' ? (

View file

@ -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) => (

Some files were not shown because too many files have changed in this diff Show more