- {servers.map((server) => (
+ {servers.map(server => (
{
{server.name}
{server.allocations
- .filter((alloc) => alloc.isDefault)
- .map((allocation) => (
+ .filter(alloc => alloc.isDefault)
+ .map(allocation => (
{allocation.alias || ip(allocation.ip)}:{allocation.port}
diff --git a/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx b/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx
index 0308b3911..9bbf30f1c 100644
--- a/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx
+++ b/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx
@@ -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';
diff --git a/resources/scripts/components/dashboard/ssh/CreateSSHKeyForm.tsx b/resources/scripts/components/dashboard/ssh/CreateSSHKeyForm.tsx
index 4b4f39cbd..f4c01c295 100644
--- a/resources/scripts/components/dashboard/ssh/CreateSSHKeyForm.tsx
+++ b/resources/scripts/components/dashboard/ssh/CreateSSHKeyForm.tsx
@@ -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));
};
diff --git a/resources/scripts/components/dashboard/ssh/DeleteSSHKeyButton.tsx b/resources/scripts/components/dashboard/ssh/DeleteSSHKeyButton.tsx
index bd9aaf0d5..96994fc5e 100644
--- a/resources/scripts/components/dashboard/ssh/DeleteSSHKeyButton.tsx
+++ b/resources/scripts/components/dashboard/ssh/DeleteSSHKeyButton.tsx
@@ -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);
});
diff --git a/resources/scripts/components/elements/AuthenticatedRoute.tsx b/resources/scripts/components/elements/AuthenticatedRoute.tsx
index 2d3b6a6b9..093427d56 100644
--- a/resources/scripts/components/elements/AuthenticatedRoute.tsx
+++ b/resources/scripts/components/elements/AuthenticatedRoute.tsx
@@ -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) => {
- const isAuthenticated = useStoreState((state) => !!state.user.data?.uuid);
+function AuthenticatedRoute({ children }: { children?: ReactNode }): JSX.Element {
+ const isAuthenticated = useStoreState(state => !!state.user.data?.uuid);
- return (
-
- isAuthenticated ? children :
- }
- />
- );
-};
+ const location = useLocation();
+
+ if (isAuthenticated) {
+ return <>{children}>;
+ }
+
+ return ;
+}
+
+export default AuthenticatedRoute;
diff --git a/resources/scripts/components/elements/Button.tsx b/resources/scripts/components/elements/Button.tsx
index e02f00ff1..f25c8b296 100644
--- a/resources/scripts/components/elements/Button.tsx
+++ b/resources/scripts/components/elements/Button.tsx
@@ -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>`
${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.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>`
}
`};
- ${(props) =>
+ ${props =>
props.color === 'green' &&
css`
${tw`border-green-600 bg-green-500 text-green-50`};
@@ -42,7 +42,7 @@ const ButtonStyle = styled.button>`
${tw`bg-green-600 border-green-700`};
}
- ${(props) =>
+ ${props =>
props.isSecondary &&
css`
&:active:not(:disabled) {
@@ -51,7 +51,7 @@ const ButtonStyle = styled.button>`
`};
`};
- ${(props) =>
+ ${props =>
props.color === 'red' &&
css`
${tw`border-red-600 bg-red-500 text-red-50`};
@@ -60,7 +60,7 @@ const ButtonStyle = styled.button>`
${tw`bg-red-600 border-red-700`};
}
- ${(props) =>
+ ${props =>
props.isSecondary &&
css`
&:active:not(:disabled) {
@@ -69,21 +69,21 @@ const ButtonStyle = styled.button>`
`};
`};
- ${(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`
${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 = ({ children, isLoading, ...props }) =>
type LinkProps = Omit & Props;
-const LinkButton: React.FC = (props) => ;
+const LinkButton: React.FC = props => ;
export { LinkButton, ButtonStyle };
export default Button;
diff --git a/resources/scripts/components/elements/Can.tsx b/resources/scripts/components/elements/Can.tsx
index 824051936..70b3fa847 100644
--- a/resources/scripts/components/elements/Can.tsx
+++ b/resources/scripts/components/elements/Can.tsx
@@ -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;
diff --git a/resources/scripts/components/elements/Checkbox.tsx b/resources/scripts/components/elements/Checkbox.tsx
index 731fd24de..1432fe1e3 100644
--- a/resources/scripts/components/elements/Checkbox.tsx
+++ b/resources/scripts/components/elements/Checkbox.tsx
@@ -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);
diff --git a/resources/scripts/components/elements/Code.tsx b/resources/scripts/components/elements/Code.tsx
index 30eac0d86..02640d699 100644
--- a/resources/scripts/components/elements/Code.tsx
+++ b/resources/scripts/components/elements/Code.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import * as React from 'react';
import classNames from 'classnames';
interface CodeProps {
diff --git a/resources/scripts/components/elements/CodemirrorEditor.tsx b/resources/scripts/components/elements/CodemirrorEditor.tsx
index 99a7b5e85..48948ad74 100644
--- a/resources/scripts/components/elements/CodemirrorEditor.tsx
+++ b/resources/scripts/components/elements/CodemirrorEditor.tsx
@@ -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();
- 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,
diff --git a/resources/scripts/components/elements/ConfirmationModal.tsx b/resources/scripts/components/elements/ConfirmationModal.tsx
index 52f9f9e3c..ffb85a13f 100644
--- a/resources/scripts/components/elements/ConfirmationModal.tsx
+++ b/resources/scripts/components/elements/ConfirmationModal.tsx
@@ -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 = ({ title, children, buttonText, onConfirmed }) => {
+function ConfirmationModal({ title, children, buttonText, onConfirmed }: Props) {
const { dismiss } = useContext(ModalContext);
return (
<>
{title}
{children}
+
dismiss()} css={tw`w-full sm:w-auto border-transparent`}>
Cancel
@@ -28,10 +33,8 @@ const ConfirmationModal: React.FC = ({ title, children, buttonText, onCon
>
);
-};
+}
-ConfirmationModal.displayName = 'ConfirmationModal';
-
-export default asModal((props) => ({
+export default asModal(props => ({
showSpinnerOverlay: props.showSpinnerOverlay,
}))(ConfirmationModal);
diff --git a/resources/scripts/components/elements/ContentBox.tsx b/resources/scripts/components/elements/ContentBox.tsx
index b829088c8..fb31f9e94 100644
--- a/resources/scripts/components/elements/ContentBox.tsx
+++ b/resources/scripts/components/elements/ContentBox.tsx
@@ -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';
diff --git a/resources/scripts/components/elements/ContentContainer.tsx b/resources/scripts/components/elements/ContentContainer.tsx
index 799f512d2..0ce40c416 100644
--- a/resources/scripts/components/elements/ContentContainer.tsx
+++ b/resources/scripts/components/elements/ContentContainer.tsx
@@ -1,4 +1,4 @@
-import styled from 'styled-components/macro';
+import styled from 'styled-components';
import { breakpoint } from '@/theme';
import tw from 'twin.macro';
diff --git a/resources/scripts/components/elements/CopyOnClick.tsx b/resources/scripts/components/elements/CopyOnClick.tsx
index 431e9c8d6..80c01277c 100644
--- a/resources/scripts/components/elements/CopyOnClick.tsx
+++ b/resources/scripts/components/elements/CopyOnClick.tsx
@@ -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 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) => {
+ onClick: (e: MouseEvent) => {
copy(String(text));
setCopied(true);
if (typeof children.props.onClick === 'function') {
@@ -46,9 +49,9 @@ const CopyOnClick = ({ text, showInNotification = true, children }: CopyOnClickP
<>
{copied && (
-
-
-
+
+
+
{showInNotification
? `Copied "${String(text)}" to clipboard.`
@@ -56,7 +59,7 @@ const CopyOnClick = ({ text, showInNotification = true, children }: CopyOnClickP
-
+
)}
{child}
diff --git a/resources/scripts/components/elements/DropdownMenu.tsx b/resources/scripts/components/elements/DropdownMenu.tsx
index 0c5361a8c..ea8208369 100644
--- a/resources/scripts/components/elements/DropdownMenu.tsx
+++ b/resources/scripts/components/elements/DropdownMenu.tsx
@@ -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
) => void) => React.ReactChild;
+ children: ReactNode;
+ renderToggle: (onClick: (e: ReactMouseEvent) => 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 {
+class DropdownMenu extends PureComponent {
menu = createRef();
- state: State = {
+ override state: State = {
posX: 0,
visible: false,
};
- componentWillUnmount() {
+ override componentWillUnmount() {
this.removeListeners();
}
- componentDidUpdate(prevProps: Readonly, prevState: Readonly) {
+ override componentDidUpdate(_prevProps: Readonly, prevState: Readonly) {
const menu = this.menu.current;
if (this.state.visible && !prevState.visible && menu) {
@@ -48,19 +50,21 @@ class DropdownMenu extends React.PureComponent {
}
}
- removeListeners = () => {
+ removeListeners() {
document.removeEventListener('click', this.windowListener);
document.removeEventListener('contextmenu', this.contextMenuListener);
- };
+ }
- onClickHandler = (e: React.MouseEvent) => {
+ onClickHandler(e: ReactMouseEvent) {
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 {
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 (
{this.props.renderToggle(this.onClickHandler)}
-
+
+
{
+ onClick={e => {
e.stopPropagation();
this.setState({ visible: false });
}}
@@ -98,7 +104,7 @@ class DropdownMenu extends React.PureComponent
{
>
{this.props.children}
-
+
);
}
diff --git a/resources/scripts/components/elements/ErrorBoundary.tsx b/resources/scripts/components/elements/ErrorBoundary.tsx
index c92d952b5..95819a02f 100644
--- a/resources/scripts/components/elements/ErrorBoundary.tsx
+++ b/resources/scripts/components/elements/ErrorBoundary.tsx
@@ -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 {
+ 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 ? (
+
An error was encountered by the application while rendering this view. Try refreshing the page.
diff --git a/resources/scripts/components/elements/Fade.tsx b/resources/scripts/components/elements/Fade.tsx
deleted file mode 100644
index 41bcf0be7..000000000
--- a/resources/scripts/components/elements/Fade.tsx
+++ /dev/null
@@ -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
{
- 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 = ({ timeout, children, ...props }) => (
-
-
- {children}
-
-
-);
-Fade.displayName = 'Fade';
-
-export default Fade;
diff --git a/resources/scripts/components/elements/Field.tsx b/resources/scripts/components/elements/Field.tsx
index 6ed5023c3..3ce78349a 100644
--- a/resources/scripts/components/elements/Field.tsx
+++ b/resources/scripts/components/elements/Field.tsx
@@ -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(
)}
- )
+ ),
);
Field.displayName = 'Field';
diff --git a/resources/scripts/components/elements/FormikFieldWrapper.tsx b/resources/scripts/components/elements/FormikFieldWrapper.tsx
index 95a645a85..91db2ff43 100644
--- a/resources/scripts/components/elements/FormikFieldWrapper.tsx
+++ b/resources/scripts/components/elements/FormikFieldWrapper.tsx
@@ -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';
diff --git a/resources/scripts/components/elements/FormikSwitch.tsx b/resources/scripts/components/elements/FormikSwitch.tsx
index 4736e0794..52faf056b 100644
--- a/resources/scripts/components/elements/FormikSwitch.tsx
+++ b/resources/scripts/components/elements/FormikSwitch.tsx
@@ -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';
diff --git a/resources/scripts/components/elements/GreyRowBox.tsx b/resources/scripts/components/elements/GreyRowBox.tsx
index e5a7f9685..99e3fadf9 100644
--- a/resources/scripts/components/elements/GreyRowBox.tsx
+++ b/resources/scripts/components/elements/GreyRowBox.tsx
@@ -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`};
diff --git a/resources/scripts/components/elements/Icon.tsx b/resources/scripts/components/elements/Icon.tsx
index 465d5a52d..d80ca96c0 100644
--- a/resources/scripts/components/elements/Icon.tsx
+++ b/resources/scripts/components/elements/Icon.tsx
@@ -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';
diff --git a/resources/scripts/components/elements/Input.tsx b/resources/scripts/components/elements/Input.tsx
index 677a5014d..c9283e055 100644
--- a/resources/scripts/components/elements/Input.tsx
+++ b/resources/scripts/components/elements/Input.tsx
@@ -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
`
& + .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`
&: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`
diff --git a/resources/scripts/components/elements/InputError.tsx b/resources/scripts/components/elements/InputError.tsx
index dfabf72d9..d39735a3a 100644
--- a/resources/scripts/components/elements/InputError.tsx
+++ b/resources/scripts/components/elements/InputError.tsx
@@ -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) =>
{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] ?? '')}
) : (
<>{children ? {children}
: null}>
diff --git a/resources/scripts/components/elements/InputSpinner.tsx b/resources/scripts/components/elements/InputSpinner.tsx
index 2ed77b433..583214a02 100644
--- a/resources/scripts/components/elements/InputSpinner.tsx
+++ b/resources/scripts/components/elements/InputSpinner.tsx
@@ -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 }) => (
-
-
-
-
-
-
- {children}
-
-);
+function InputSpinner({ visible, children }: { visible: boolean; children: ReactNode }) {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+ );
+}
export default InputSpinner;
diff --git a/resources/scripts/components/elements/Label.tsx b/resources/scripts/components/elements/Label.tsx
index 704e8324b..c97e99c65 100644
--- a/resources/scripts/components/elements/Label.tsx
+++ b/resources/scripts/components/elements/Label.tsx
@@ -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;
diff --git a/resources/scripts/components/elements/Modal.tsx b/resources/scripts/components/elements/Modal.tsx
index d4f6dfece..89de26562 100644
--- a/resources/scripts/components/elements/Modal.tsx
+++ b/resources/scripts/components/elements/Modal.tsx
@@ -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 = ({
+function Modal({
visible,
appear,
dismissable,
@@ -65,7 +69,7 @@ const Modal: React.FC = ({
closeOnEscape = true,
onDismissed,
children,
-}) => {
+}: ModalProps) {
const [render, setRender] = useState(visible);
const isDismissable = useMemo(() => {
@@ -85,14 +89,20 @@ const Modal: React.FC = ({
};
}, [isDismissable, closeOnEscape, render]);
- useEffect(() => setRender(visible), [visible]);
+ useEffect(() => {
+ setRender(visible);
+
+ if (!visible) {
+ onDismissed();
+ }
+ }, [visible]);
return (
- onDismissed()}>
+
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 = ({
)}
- {showSpinnerOverlay && (
-
-
-
-
-
- )}
+
+
+
+
+
+
+
@@ -136,14 +146,14 @@ const Modal: React.FC = ({
-
+
);
-};
+}
-const PortaledModal: React.FC = ({ children, ...props }) => {
+function PortaledModal({ children, ...props }: ModalProps): JSX.Element {
const element = useRef(document.getElementById('modal-portal'));
return createPortal({children} , element.current!);
-};
+}
export default PortaledModal;
diff --git a/resources/scripts/components/elements/PageContentBlock.tsx b/resources/scripts/components/elements/PageContentBlock.tsx
index a4df19567..9def94e52 100644
--- a/resources/scripts/components/elements/PageContentBlock.tsx
+++ b/resources/scripts/components/elements/PageContentBlock.tsx
@@ -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 = ({ 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 = ({ title, showFlashKey
}, [title]);
return (
-
- <>
-
- {showFlashKey && }
- {children}
-
-
-
-
- Pterodactyl®
-
- © 2015 - {new Date().getFullYear()}
-
-
- >
-
+ <>
+
+ {showFlashKey && }
+ {children}
+
+
+
+
+
+ Pterodactyl®
+
+ © 2015 - {new Date().getFullYear()}
+
+
+ >
);
-};
+}
export default PageContentBlock;
diff --git a/resources/scripts/components/elements/Pagination.tsx b/resources/scripts/components/elements/Pagination.tsx
index 7a2951f77..0bb395ea8 100644
--- a/resources/scripts/components/elements/Pagination.tsx
+++ b/resources/scripts/components/elements/Pagination.tsx
@@ -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({ data: { items, pagination }, onPageSelect, children }:
{children({ items, isFirstPage, isLastPage })}
{pages.length > 1 && (
- {pages[0] > 1 && !isFirstPage && (
+ {(pages?.[0] ?? 0) > 1 && !isFirstPage && (
onPageSelect(1)}>
)}
- {pages.map((i) => (
+ {pages.map(i => (
({ data: { items, pagination }, onPageSelect, children }:
{i}
))}
- {pages[4] < pagination.totalPages && !isLastPage && (
+ {(pages?.[4] ?? 0) < pagination.totalPages && !isLastPage && (
onPageSelect(pagination.totalPages)}>
diff --git a/resources/scripts/components/elements/PermissionRoute.tsx b/resources/scripts/components/elements/PermissionRoute.tsx
index e7a46e570..ed50db852 100644
--- a/resources/scripts/components/elements/PermissionRoute.tsx
+++ b/resources/scripts/components/elements/PermissionRoute.tsx
@@ -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
{
- 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) => (
-
- {!permission ? (
- children
- ) : (
-
- }
- >
- {children}
-
- )}
-
-);
+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 ;
+}
+
+export default PermissionRoute;
diff --git a/resources/scripts/components/elements/Portal.tsx b/resources/scripts/components/elements/Portal.tsx
index e0be75778..1bbc03e4c 100644
--- a/resources/scripts/components/elements/Portal.tsx
+++ b/resources/scripts/components/elements/Portal.tsx
@@ -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 }) => {
diff --git a/resources/scripts/components/elements/ProgressBar.tsx b/resources/scripts/components/elements/ProgressBar.tsx
index c94ae80f3..9ad271972 100644
--- a/resources/scripts/components/elements/ProgressBar.tsx
+++ b/resources/scripts/components/elements/ProgressBar.tsx
@@ -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;
-export default () => {
- const interval = useRef(null) as React.MutableRefObject;
- const timeout = useRef(null) as React.MutableRefObject;
+function ProgressBar() {
+ const interval = useRef();
+ const timeout = useRef();
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 (
-
-
-
-
+
);
-};
+}
+
+export default ProgressBar;
diff --git a/resources/scripts/components/elements/ScreenBlock.tsx b/resources/scripts/components/elements/ScreenBlock.tsx
index 0b57e92c6..26e8ec5ac 100644
--- a/resources/scripts/components/elements/ScreenBlock.tsx
+++ b/resources/scripts/components/elements/ScreenBlock.tsx
@@ -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';
diff --git a/resources/scripts/components/elements/Select.tsx b/resources/scripts/components/elements/Select.tsx
index fc50f252d..de74a8aef 100644
--- a/resources/scripts/components/elements/Select.tsx
+++ b/resources/scripts/components/elements/Select.tsx
@@ -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
`
display: none;
}
- ${(props) =>
+ ${props =>
!props.hideDropdownArrow &&
css`
${tw`bg-neutral-600 border-neutral-500 text-neutral-200`};
diff --git a/resources/scripts/components/elements/ServerContentBlock.tsx b/resources/scripts/components/elements/ServerContentBlock.tsx
index cda208c86..6905843f9 100644
--- a/resources/scripts/components/elements/ServerContentBlock.tsx
+++ b/resources/scripts/components/elements/ServerContentBlock.tsx
@@ -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 = ({ title, children, ...props }) => {
- const name = ServerContext.useStoreState((state) => state.server.data!.name);
+ const name = ServerContext.useStoreState(state => state.server.data!.name);
return (
diff --git a/resources/scripts/components/elements/Spinner.tsx b/resources/scripts/components/elements/Spinner.tsx
index 48de135d3..fe5ae6401 100644
--- a/resources/scripts/components/elements/Spinner.tsx
+++ b/resources/scripts/components/elements/Spinner.tsx
@@ -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 {
+interface Spinner extends FC {
Size: Record<'SMALL' | 'BASE' | 'LARGE', SpinnerSize>;
- Suspense: React.FC;
+ Suspense: FC;
}
const spin = keyframes`
@@ -27,7 +31,7 @@ const SpinnerComponent = styled.div`
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`
`
: 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 }) =>
diff --git a/resources/scripts/components/elements/SpinnerOverlay.tsx b/resources/scripts/components/elements/SpinnerOverlay.tsx
index d2052f3be..0775a71f7 100644
--- a/resources/scripts/components/elements/SpinnerOverlay.tsx
+++ b/resources/scripts/components/elements/SpinnerOverlay.tsx
@@ -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 = ({ size, fixed, visible, backgroundOpacity, children }) => (
-
+function SpinnerOverlay({ size, fixed, visible, backgroundOpacity, children }: Props) {
+ if (!visible) {
+ return null;
+ }
+
+ return (
= ({ size, fixed, visible, backgroundOpaci
{children && (typeof children === 'string' ?
{children}
: children)}
-
-);
+ );
+}
export default SpinnerOverlay;
diff --git a/resources/scripts/components/elements/SubNavigation.tsx b/resources/scripts/components/elements/SubNavigation.tsx
index 9a25b80b0..c28ae7ba3 100644
--- a/resources/scripts/components/elements/SubNavigation.tsx
+++ b/resources/scripts/components/elements/SubNavigation.tsx
@@ -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`
diff --git a/resources/scripts/components/elements/Switch.tsx b/resources/scripts/components/elements/Switch.tsx
index 45c738474..03927fd63 100644
--- a/resources/scripts/components/elements/Switch.tsx
+++ b/resources/scripts/components/elements/Switch.tsx
@@ -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) => void;
- children?: React.ReactNode;
+ onChange?: (e: ChangeEvent) => void;
+ children?: ReactNode;
}
const Switch = ({ name, label, description, defaultChecked, readOnly, onChange, children }: SwitchProps) => {
- const uuid = useMemo(() => v4(), []);
+ const uuid = useMemo(() => nanoid(), []);
return (
@@ -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}
/>
diff --git a/resources/scripts/components/elements/TitledGreyBox.tsx b/resources/scripts/components/elements/TitledGreyBox.tsx
index e135549b1..9ccabcc71 100644
--- a/resources/scripts/components/elements/TitledGreyBox.tsx
+++ b/resources/scripts/components/elements/TitledGreyBox.tsx
@@ -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';
diff --git a/resources/scripts/components/elements/Translate.tsx b/resources/scripts/components/elements/Translate.tsx
index 88dd1eab6..fac25eca8 100644
--- a/resources/scripts/components/elements/Translate.tsx
+++ b/resources/scripts/components/elements/Translate.tsx
@@ -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
;
+type Props = Omit, '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}
);
-};
+}
+
+export default Translate;
diff --git a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx
index 183b2ab3f..8f9794a81 100644
--- a/resources/scripts/components/elements/activity/ActivityLogEntry.tsx
+++ b/resources/scripts/components/elements/activity/ActivityLogEntry.tsx
@@ -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';
diff --git a/resources/scripts/components/elements/activity/ActivityLogMetaButton.tsx b/resources/scripts/components/elements/activity/ActivityLogMetaButton.tsx
index f1460ac7c..aebe3e9f8 100644
--- a/resources/scripts/components/elements/activity/ActivityLogMetaButton.tsx
+++ b/resources/scripts/components/elements/activity/ActivityLogMetaButton.tsx
@@ -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';
diff --git a/resources/scripts/components/elements/alert/Alert.tsx b/resources/scripts/components/elements/alert/Alert.tsx
index e99d6d57e..2252e71a0 100644
--- a/resources/scripts/components/elements/alert/Alert.tsx
+++ b/resources/scripts/components/elements/alert/Alert.tsx
@@ -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' ? (
diff --git a/resources/scripts/components/elements/button/Button.tsx b/resources/scripts/components/elements/button/Button.tsx
index 61ee96311..6b140ddf1 100644
--- a/resources/scripts/components/elements/button/Button.tsx
+++ b/resources/scripts/components/elements/button/Button.tsx
@@ -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(
[styles.small]: size === Options.Size.Small,
[styles.large]: size === Options.Size.Large,
},
- className
+ className,
)}
{...rest}
>
{children}
);
- }
+ },
);
const TextButton = forwardRef(({ className, ...props }, ref) => (
diff --git a/resources/scripts/components/elements/button/types.ts b/resources/scripts/components/elements/button/types.ts
index 273ac3b67..e86ec5e26 100644
--- a/resources/scripts/components/elements/button/types.ts
+++ b/resources/scripts/components/elements/button/types.ts
@@ -1,15 +1,15 @@
-enum Shape {
+export enum Shape {
Default,
IconSquare,
}
-enum Size {
+export enum Size {
Default,
Small,
Large,
}
-enum Variant {
+export enum Variant {
Primary,
Secondary,
}
diff --git a/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx b/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx
index bedfe4ee5..3f2cd7217 100644
--- a/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx
+++ b/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import * as React from 'react';
import { Dialog, RenderDialogProps } from './';
import { Button } from '@/components/elements/button/index';
diff --git a/resources/scripts/components/elements/dialog/Dialog.tsx b/resources/scripts/components/elements/dialog/Dialog.tsx
index bb35fe8fb..7d347063e 100644
--- a/resources/scripts/components/elements/dialog/Dialog.tsx
+++ b/resources/scripts/components/elements/dialog/Dialog.tsx
@@ -1,4 +1,5 @@
-import React, { useRef, useState } from 'react';
+import { useRef, useState } from 'react';
+import * as React from 'react';
import { Dialog as HDialog } from '@headlessui/react';
import { Button } from '@/components/elements/button/index';
import { XIcon } from '@heroicons/react/solid';
diff --git a/resources/scripts/components/elements/dialog/DialogFooter.tsx b/resources/scripts/components/elements/dialog/DialogFooter.tsx
index 37209fce8..de0a32f91 100644
--- a/resources/scripts/components/elements/dialog/DialogFooter.tsx
+++ b/resources/scripts/components/elements/dialog/DialogFooter.tsx
@@ -1,4 +1,5 @@
-import React, { useContext } from 'react';
+import { useContext } from 'react';
+import * as React from 'react';
import { DialogContext } from './';
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
@@ -7,7 +8,7 @@ export default ({ children }: { children: React.ReactNode }) => {
useDeepCompareEffect(() => {
setFooter(
- {children}
+ {children}
,
);
}, [children]);
diff --git a/resources/scripts/components/elements/dialog/DialogIcon.tsx b/resources/scripts/components/elements/dialog/DialogIcon.tsx
index e2ed6b207..d075e4781 100644
--- a/resources/scripts/components/elements/dialog/DialogIcon.tsx
+++ b/resources/scripts/components/elements/dialog/DialogIcon.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect } from 'react';
+import { useContext, useEffect } from 'react';
import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
import classNames from 'classnames';
import { DialogContext, DialogIconProps, styles } from './';
@@ -19,7 +19,7 @@ export default ({ type, position, className }: DialogIconProps) => {
setIcon(
-
+ ,
);
}, [type, className]);
diff --git a/resources/scripts/components/elements/dialog/context.ts b/resources/scripts/components/elements/dialog/context.ts
index b9a0f0980..dd270e149 100644
--- a/resources/scripts/components/elements/dialog/context.ts
+++ b/resources/scripts/components/elements/dialog/context.ts
@@ -1,13 +1,13 @@
-import React from 'react';
+import { createContext } from 'react';
import { DialogContextType, DialogWrapperContextType } from './types';
-export const DialogContext = React.createContext({
+export const DialogContext = createContext({
setIcon: () => null,
setFooter: () => null,
setIconPosition: () => null,
});
-export const DialogWrapperContext = React.createContext({
+export const DialogWrapperContext = createContext({
props: {},
setProps: () => null,
close: () => null,
diff --git a/resources/scripts/components/elements/dialog/types.d.ts b/resources/scripts/components/elements/dialog/types.d.ts
index 2701b1dda..bb5808949 100644
--- a/resources/scripts/components/elements/dialog/types.d.ts
+++ b/resources/scripts/components/elements/dialog/types.d.ts
@@ -1,4 +1,4 @@
-import React from 'react';
+import * as React from 'react';
import { IconPosition } from '@/components/elements/dialog/DialogIcon';
type Callback = ((value: T) => void) | React.Dispatch>;
diff --git a/resources/scripts/components/elements/dropdown/Dropdown.tsx b/resources/scripts/components/elements/dropdown/Dropdown.tsx
index ef378efe5..df77921df 100644
--- a/resources/scripts/components/elements/dropdown/Dropdown.tsx
+++ b/resources/scripts/components/elements/dropdown/Dropdown.tsx
@@ -1,4 +1,5 @@
-import React, { ElementType, forwardRef, useMemo } from 'react';
+import { ElementType, forwardRef, useMemo } from 'react';
+import * as React from 'react';
import { Menu, Transition } from '@headlessui/react';
import styles from './style.module.css';
import classNames from 'classnames';
@@ -23,8 +24,8 @@ const Dropdown = forwardRef(({ as, children }, ref) => {
const list = React.Children.toArray(children) as unknown as TypedChild[];
return [
- list.filter((child) => child.type === DropdownButton),
- list.filter((child) => child.type !== DropdownButton),
+ list.filter(child => child.type === DropdownButton),
+ list.filter(child => child.type !== DropdownButton),
];
}, [children]);
diff --git a/resources/scripts/components/elements/dropdown/DropdownButton.tsx b/resources/scripts/components/elements/dropdown/DropdownButton.tsx
index d026e2995..13e3e38fc 100644
--- a/resources/scripts/components/elements/dropdown/DropdownButton.tsx
+++ b/resources/scripts/components/elements/dropdown/DropdownButton.tsx
@@ -2,7 +2,7 @@ import classNames from 'classnames';
import styles from '@/components/elements/dropdown/style.module.css';
import { ChevronDownIcon } from '@heroicons/react/solid';
import { Menu } from '@headlessui/react';
-import React from 'react';
+import * as React from 'react';
interface Props {
className?: string;
diff --git a/resources/scripts/components/elements/dropdown/DropdownItem.tsx b/resources/scripts/components/elements/dropdown/DropdownItem.tsx
index 1c012e15a..d9f0cf68f 100644
--- a/resources/scripts/components/elements/dropdown/DropdownItem.tsx
+++ b/resources/scripts/components/elements/dropdown/DropdownItem.tsx
@@ -1,4 +1,5 @@
-import React, { forwardRef } from 'react';
+import { forwardRef } from 'react';
+import * as React from 'react';
import { Menu } from '@headlessui/react';
import styles from './style.module.css';
import classNames from 'classnames';
@@ -26,7 +27,7 @@ const DropdownItem = forwardRef(
[styles.danger]: danger,
[styles.disabled]: disabled,
},
- className
+ className,
)}
onClick={onClick}
>
@@ -36,7 +37,7 @@ const DropdownItem = forwardRef(
)}
);
- }
+ },
);
export default DropdownItem;
diff --git a/resources/scripts/components/elements/editor/Editor.tsx b/resources/scripts/components/elements/editor/Editor.tsx
new file mode 100644
index 000000000..bc395d601
--- /dev/null
+++ b/resources/scripts/components/elements/editor/Editor.tsx
@@ -0,0 +1,206 @@
+import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
+import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
+import {
+ defaultHighlightStyle,
+ syntaxHighlighting,
+ indentOnInput,
+ bracketMatching,
+ foldGutter,
+ foldKeymap,
+ LanguageDescription,
+ indentUnit,
+} from '@codemirror/language';
+import { languages } from '@codemirror/language-data';
+import { lintKeymap } from '@codemirror/lint';
+import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
+import type { Extension } from '@codemirror/state';
+import { EditorState, Compartment } from '@codemirror/state';
+import {
+ keymap,
+ highlightSpecialChars,
+ drawSelection,
+ highlightActiveLine,
+ dropCursor,
+ rectangularSelection,
+ crosshairCursor,
+ lineNumbers,
+ highlightActiveLineGutter,
+ EditorView,
+} from '@codemirror/view';
+import type { CSSProperties } from 'react';
+import { useEffect, useRef, useState } from 'react';
+
+import { ayuMirageHighlightStyle, ayuMirageTheme } from './theme';
+
+function findLanguageByFilename(filename: string): LanguageDescription | undefined {
+ const language = LanguageDescription.matchFilename(languages, filename);
+ if (language !== null) {
+ return language;
+ }
+
+ return undefined;
+}
+
+const defaultExtensions: Extension = [
+ // Ayu Mirage
+ ayuMirageTheme,
+ syntaxHighlighting(ayuMirageHighlightStyle),
+
+ lineNumbers(),
+ highlightActiveLineGutter(),
+ highlightSpecialChars(),
+ history(),
+ foldGutter(),
+ drawSelection(),
+ dropCursor(),
+ EditorState.allowMultipleSelections.of(true),
+ indentOnInput(),
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
+ bracketMatching(),
+ closeBrackets(),
+ autocompletion(),
+ rectangularSelection(),
+ crosshairCursor(),
+ highlightActiveLine(),
+ highlightSelectionMatches(),
+ keymap.of([
+ ...closeBracketsKeymap,
+ ...defaultKeymap,
+ ...searchKeymap,
+ ...historyKeymap,
+ ...foldKeymap,
+ ...completionKeymap,
+ ...lintKeymap,
+ indentWithTab,
+ ]),
+ EditorState.tabSize.of(4),
+ indentUnit.of('\t'),
+];
+
+export interface EditorProps {
+ // DOM
+ className?: string;
+ style?: CSSProperties;
+
+ // CodeMirror Config
+ extensions?: Extension[];
+ language?: LanguageDescription;
+
+ // Options
+ filename?: string;
+ initialContent?: string;
+
+ // ?
+ fetchContent?: (callback: () => Promise) => void;
+
+ // Events
+ onContentSaved?: () => void;
+ onLanguageChanged?: (language: LanguageDescription | undefined) => void;
+}
+
+export function Editor(props: EditorProps) {
+ const ref = useRef(null);
+
+ const [view, setView] = useState();
+
+ // eslint-disable-next-line react/hook-use-state
+ const [languageConfig] = useState(new Compartment());
+ // eslint-disable-next-line react/hook-use-state
+ const [keybindings] = useState(new Compartment());
+
+ const createEditorState = () =>
+ EditorState.create({
+ doc: props.initialContent,
+ extensions: [
+ defaultExtensions,
+ props.extensions === undefined ? [] : props.extensions,
+ languageConfig.of([]),
+ keybindings.of([]),
+ ],
+ });
+
+ useEffect(() => {
+ if (ref.current === null) {
+ return;
+ }
+
+ setView(
+ new EditorView({
+ state: createEditorState(),
+ parent: ref.current,
+ }),
+ );
+
+ return () => {
+ if (view === undefined) {
+ return;
+ }
+
+ view.destroy();
+ setView(undefined);
+ };
+ }, [ref]);
+
+ useEffect(() => {
+ if (view === undefined) {
+ return;
+ }
+
+ view.setState(createEditorState());
+ }, [props.initialContent]);
+
+ useEffect(() => {
+ if (view === undefined) {
+ return;
+ }
+
+ const language = props.language ?? findLanguageByFilename(props.filename ?? '');
+ if (language === undefined) {
+ return;
+ }
+
+ void language.load().then(language => {
+ view.dispatch({
+ effects: languageConfig.reconfigure(language),
+ });
+ });
+
+ if (props.onLanguageChanged !== undefined) {
+ props.onLanguageChanged(language);
+ }
+ }, [view, props.filename, props.language]);
+
+ useEffect(() => {
+ if (props.fetchContent === undefined) {
+ return;
+ }
+
+ if (!view) {
+ props.fetchContent(async () => {
+ throw new Error('no editor session has been configured');
+ });
+ return;
+ }
+
+ const { onContentSaved } = props;
+ if (onContentSaved !== undefined) {
+ view.dispatch({
+ effects: keybindings.reconfigure(
+ keymap.of([
+ {
+ key: 'Mod-s',
+ run() {
+ onContentSaved();
+ return true;
+ },
+ },
+ ]),
+ ),
+ });
+ }
+
+ props.fetchContent(async () => view.state.doc.toJSON().join('\n'));
+ }, [view, props.fetchContent, props.onContentSaved]);
+
+ return
;
+}
diff --git a/resources/scripts/components/elements/editor/index.ts b/resources/scripts/components/elements/editor/index.ts
new file mode 100644
index 000000000..5ccde0efc
--- /dev/null
+++ b/resources/scripts/components/elements/editor/index.ts
@@ -0,0 +1 @@
+export { Editor } from './Editor';
diff --git a/resources/scripts/components/elements/editor/theme.ts b/resources/scripts/components/elements/editor/theme.ts
new file mode 100644
index 000000000..7e212fca1
--- /dev/null
+++ b/resources/scripts/components/elements/editor/theme.ts
@@ -0,0 +1,148 @@
+import { HighlightStyle } from '@codemirror/language';
+import type { Extension } from '@codemirror/state';
+import { EditorView } from '@codemirror/view';
+import { tags as t } from '@lezer/highlight';
+
+const highlightBackground = 'transparent';
+const background = '#1F2430';
+const selection = '#34455A';
+const cursor = '#FFCC66';
+
+export const ayuMirageTheme: Extension = EditorView.theme(
+ {
+ '&': {
+ color: '#CBCCC6',
+ backgroundColor: background,
+ },
+
+ '.cm-content': {
+ caretColor: cursor,
+ },
+
+ '&.cm-focused .cm-cursor': { borderLeftColor: cursor },
+ '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
+ backgroundColor: selection,
+ },
+
+ '.cm-panels': { backgroundColor: '#232834', color: '#CBCCC6' },
+ '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
+ '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
+
+ '.cm-searchMatch': {
+ backgroundColor: '#72a1ff59',
+ outline: '1px solid #457dff',
+ },
+ '.cm-searchMatch.cm-searchMatch-selected': {
+ backgroundColor: '#6199ff2f',
+ },
+
+ '.cm-activeLine': { backgroundColor: highlightBackground },
+ '.cm-selectionMatch': { backgroundColor: '#aafe661a' },
+
+ '.cm-matchingBracket, .cm-nonmatchingBracket': {
+ backgroundColor: '#bad0f847',
+ outline: '1px solid #515a6b',
+ },
+
+ '.cm-gutters': {
+ backgroundColor: 'transparent',
+ color: '#FF3333',
+ border: 'none',
+ },
+
+ '.cm-gutterElement': {
+ color: 'rgba(61, 66, 77, 99)',
+ },
+
+ '.cm-activeLineGutter': {
+ backgroundColor: highlightBackground,
+ },
+
+ '.cm-foldPlaceholder': {
+ backgroundColor: 'transparent',
+ border: 'none',
+ color: '#ddd',
+ },
+
+ '.cm-tooltip': {
+ border: '1px solid #181a1f',
+ backgroundColor: '#232834',
+ },
+ '.cm-tooltip-autocomplete': {
+ '& > ul > li[aria-selected]': {
+ backgroundColor: highlightBackground,
+ color: '#CBCCC6',
+ },
+ },
+ },
+ { dark: true },
+);
+
+export const ayuMirageHighlightStyle = HighlightStyle.define([
+ {
+ tag: t.keyword,
+ color: '#FFA759',
+ },
+ {
+ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
+ color: '#5CCFE6',
+ },
+ {
+ tag: [t.function(t.variableName), t.labelName],
+ color: '#CBCCC6',
+ },
+ {
+ tag: [t.color, t.constant(t.name), t.standard(t.name)],
+ color: '#F29E74',
+ },
+ {
+ tag: [t.definition(t.name), t.separator],
+ color: '#CBCCC6B3',
+ },
+ {
+ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
+ color: '#FFCC66',
+ },
+ {
+ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)],
+ color: '#5CCFE6',
+ },
+ {
+ tag: [t.meta, t.comment],
+ color: '#5C6773',
+ },
+ {
+ tag: t.strong,
+ fontWeight: 'bold',
+ },
+ {
+ tag: t.emphasis,
+ fontStyle: 'italic',
+ },
+ {
+ tag: t.strikethrough,
+ textDecoration: 'line-through',
+ },
+ {
+ tag: t.link,
+ color: '#FF3333',
+ textDecoration: 'underline',
+ },
+ {
+ tag: t.heading,
+ fontWeight: 'bold',
+ color: '#BAE67E',
+ },
+ {
+ tag: [t.atom, t.bool, t.special(t.variableName)],
+ color: '#5CCFE6',
+ },
+ {
+ tag: [t.processingInstruction, t.string, t.inserted],
+ color: '#BAE67E',
+ },
+ {
+ tag: t.invalid,
+ color: '#FF3333',
+ },
+]);
diff --git a/resources/scripts/components/elements/inputs/Checkbox.tsx b/resources/scripts/components/elements/inputs/Checkbox.tsx
index 82233b15f..e9c043b61 100644
--- a/resources/scripts/components/elements/inputs/Checkbox.tsx
+++ b/resources/scripts/components/elements/inputs/Checkbox.tsx
@@ -1,4 +1,5 @@
-import React, { forwardRef } from 'react';
+import { forwardRef } from 'react';
+import * as React from 'react';
import classNames from 'classnames';
import styles from './styles.module.css';
diff --git a/resources/scripts/components/elements/inputs/InputField.tsx b/resources/scripts/components/elements/inputs/InputField.tsx
index c3dfc3ccd..ac92e17f1 100644
--- a/resources/scripts/components/elements/inputs/InputField.tsx
+++ b/resources/scripts/components/elements/inputs/InputField.tsx
@@ -1,4 +1,5 @@
-import React, { forwardRef } from 'react';
+import { forwardRef } from 'react';
+import * as React from 'react';
import classNames from 'classnames';
import styles from './styles.module.css';
@@ -19,7 +20,7 @@ const Component = forwardRef(({ className, va
'form-input',
styles.text_input,
{ [styles.loose]: variant === Variant.Loose },
- className
+ className,
)}
{...props}
/>
diff --git a/resources/scripts/components/elements/inputs/index.ts b/resources/scripts/components/elements/inputs/index.ts
index b961b0764..3b7f15d77 100644
--- a/resources/scripts/components/elements/inputs/index.ts
+++ b/resources/scripts/components/elements/inputs/index.ts
@@ -6,7 +6,7 @@ const Input = Object.assign(
{
Text: InputField,
Checkbox: Checkbox,
- }
+ },
);
export { Input };
diff --git a/resources/scripts/components/elements/table/PaginationFooter.tsx b/resources/scripts/components/elements/table/PaginationFooter.tsx
index 0484065ed..d578ae4d9 100644
--- a/resources/scripts/components/elements/table/PaginationFooter.tsx
+++ b/resources/scripts/components/elements/table/PaginationFooter.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import { PaginationDataSet } from '@/api/http';
import classNames from 'classnames';
import { Button } from '@/components/elements/button/index';
@@ -53,7 +52,7 @@ const PaginationFooter = ({ pagination, className, onPageSelect }: Props) => {
- {pages.previous.reverse().map((value) => (
+ {pages.previous.reverse().map(value => (
{value}
@@ -61,7 +60,7 @@ const PaginationFooter = ({ pagination, className, onPageSelect }: Props) => {
{current}
- {pages.next.map((value) => (
+ {pages.next.map(value => (
{value}
diff --git a/resources/scripts/components/elements/tooltip/Tooltip.tsx b/resources/scripts/components/elements/tooltip/Tooltip.tsx
index ce488dc4f..df24d38cc 100644
--- a/resources/scripts/components/elements/tooltip/Tooltip.tsx
+++ b/resources/scripts/components/elements/tooltip/Tooltip.tsx
@@ -1,4 +1,5 @@
-import React, { cloneElement, useRef, useState } from 'react';
+import { cloneElement, useRef, useState } from 'react';
+import * as React from 'react';
import {
arrow,
autoUpdate,
@@ -104,7 +105,7 @@ export default ({ children, ...props }: Props) => {
ref={arrowEl}
style={{
transform: `translate(${Math.round(ax || 0)}px, ${Math.round(
- ay || 0
+ ay || 0,
)}px) rotate(45deg)`,
}}
className={classNames('absolute bg-gray-900 w-3 h-3', side)}
diff --git a/resources/scripts/components/elements/transitions/FadeTransition.tsx b/resources/scripts/components/elements/transitions/FadeTransition.tsx
index e4287cc18..be0dd6425 100644
--- a/resources/scripts/components/elements/transitions/FadeTransition.tsx
+++ b/resources/scripts/components/elements/transitions/FadeTransition.tsx
@@ -1,16 +1,18 @@
-import React from 'react';
import { Transition } from '@headlessui/react';
+import type { ElementType, ReactNode } from 'react';
type Duration = `duration-${number}`;
interface Props {
- as?: React.ElementType;
+ as?: ElementType;
duration?: Duration | [Duration, Duration];
+ appear?: boolean;
+ unmount?: boolean;
show: boolean;
- children: React.ReactNode;
+ children: ReactNode;
}
-export default ({ children, duration, ...props }: Props) => {
+function FadeTransition({ children, duration, ...props }: Props) {
const [enterDuration, exitDuration] = Array.isArray(duration)
? duration
: !duration
@@ -30,4 +32,6 @@ export default ({ children, duration, ...props }: Props) => {
{children}
);
-};
+}
+
+export default FadeTransition;
diff --git a/resources/scripts/components/history.ts b/resources/scripts/components/history.ts
deleted file mode 100644
index 5f339d963..000000000
--- a/resources/scripts/components/history.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { createBrowserHistory } from 'history';
-
-export const history = createBrowserHistory({ basename: '/' });
diff --git a/resources/scripts/components/server/ConflictStateRenderer.tsx b/resources/scripts/components/server/ConflictStateRenderer.tsx
index 95e70bbaa..8bba31ca7 100644
--- a/resources/scripts/components/server/ConflictStateRenderer.tsx
+++ b/resources/scripts/components/server/ConflictStateRenderer.tsx
@@ -1,15 +1,14 @@
-import React from 'react';
-import { ServerContext } from '@/state/server';
-import ScreenBlock from '@/components/elements/ScreenBlock';
import ServerInstallSvg from '@/assets/images/server_installing.svg';
import ServerErrorSvg from '@/assets/images/server_error.svg';
import ServerRestoreSvg from '@/assets/images/server_restore.svg';
+import ScreenBlock from '@/components/elements/ScreenBlock';
+import { ServerContext } from '@/state/server';
export default () => {
- const status = ServerContext.useStoreState((state) => state.server.data?.status || null);
- const isTransferring = ServerContext.useStoreState((state) => state.server.data?.isTransferring || false);
+ const status = ServerContext.useStoreState(state => state.server.data?.status || null);
+ const isTransferring = ServerContext.useStoreState(state => state.server.data?.isTransferring || false);
const isNodeUnderMaintenance = ServerContext.useStoreState(
- (state) => state.server.data?.isNodeUnderMaintenance || false
+ state => state.server.data?.isNodeUnderMaintenance || false,
);
return status === 'installing' || status === 'install_failed' ? (
@@ -36,7 +35,7 @@ export default () => {
image={ServerRestoreSvg}
message={
isTransferring
- ? 'Your server is being transfered to a new node, please check back later.'
+ ? 'Your server is being transferred to a new node, please check back later.'
: 'Your server is currently being restored from a backup, please check back in a few minutes.'
}
/>
diff --git a/resources/scripts/components/server/InstallListener.tsx b/resources/scripts/components/server/InstallListener.tsx
index b4e3a0e3e..fbf377a2c 100644
--- a/resources/scripts/components/server/InstallListener.tsx
+++ b/resources/scripts/components/server/InstallListener.tsx
@@ -5,26 +5,26 @@ import { mutate } from 'swr';
import { getDirectorySwrKey } from '@/plugins/useFileManagerSwr';
const InstallListener = () => {
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
- const getServer = ServerContext.useStoreActions((actions) => actions.server.getServer);
- const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
+ const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
+ const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
useWebsocketEvent(SocketEvent.BACKUP_RESTORE_COMPLETED, () => {
mutate(getDirectorySwrKey(uuid, '/'), undefined);
- setServerFromState((s) => ({ ...s, status: null }));
+ setServerFromState(s => ({ ...s, status: null }));
});
// Listen for the installation completion event and then fire off a request to fetch the updated
// server information. This allows the server to automatically become available to the user if they
// just sit on the page.
useWebsocketEvent(SocketEvent.INSTALL_COMPLETED, () => {
- getServer(uuid).catch((error) => console.error(error));
+ getServer(uuid).catch(error => console.error(error));
});
// When we see the install started event immediately update the state to indicate such so that the
// screens automatically update.
useWebsocketEvent(SocketEvent.INSTALL_STARTED, () => {
- setServerFromState((s) => ({ ...s, status: 'installing' }));
+ setServerFromState(s => ({ ...s, status: 'installing' }));
});
return null;
diff --git a/resources/scripts/components/server/ServerActivityLogContainer.tsx b/resources/scripts/components/server/ServerActivityLogContainer.tsx
index 531945486..a617d1d2d 100644
--- a/resources/scripts/components/server/ServerActivityLogContainer.tsx
+++ b/resources/scripts/components/server/ServerActivityLogContainer.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { useActivityLogs } from '@/api/server/activity';
import ServerContentBlock from '@/components/elements/ServerContentBlock';
import { useFlashKey } from '@/plugins/useFlash';
@@ -24,7 +24,7 @@ export default () => {
});
useEffect(() => {
- setFilters((value) => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
+ setFilters(value => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
}, [hash]);
useEffect(() => {
@@ -39,7 +39,7 @@ export default () => {
setFilters((value) => ({ ...value, filters: {} }))}
+ onClick={() => setFilters(value => ({ ...value, filters: {} }))}
>
Clear Filters
@@ -51,7 +51,7 @@ export default () => {
No activity logs available for this server.
) : (
- {data?.items.map((activity) => (
+ {data?.items.map(activity => (
@@ -61,7 +61,7 @@ export default () => {
{data && (
setFilters((value) => ({ ...value, page }))}
+ onPageSelect={page => setFilters(value => ({ ...value, page }))}
/>
)}
diff --git a/resources/scripts/components/server/TransferListener.tsx b/resources/scripts/components/server/TransferListener.tsx
index 4d9421745..10544f547 100644
--- a/resources/scripts/components/server/TransferListener.tsx
+++ b/resources/scripts/components/server/TransferListener.tsx
@@ -3,19 +3,19 @@ import { ServerContext } from '@/state/server';
import { SocketEvent } from '@/components/server/events';
const TransferListener = () => {
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
- const getServer = ServerContext.useStoreActions((actions) => actions.server.getServer);
- const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
+ const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
+ const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
// Listen for the transfer status event, so we can update the state of the server.
useWebsocketEvent(SocketEvent.TRANSFER_STATUS, (status: string) => {
if (status === 'pending' || status === 'processing') {
- setServerFromState((s) => ({ ...s, isTransferring: true }));
+ setServerFromState(s => ({ ...s, isTransferring: true }));
return;
}
if (status === 'failed') {
- setServerFromState((s) => ({ ...s, isTransferring: false }));
+ setServerFromState(s => ({ ...s, isTransferring: false }));
return;
}
@@ -24,7 +24,7 @@ const TransferListener = () => {
}
// Refresh the server's information as it's node and allocations were just updated.
- getServer(uuid).catch((error) => console.error(error));
+ getServer(uuid).catch(error => console.error(error));
});
return null;
diff --git a/resources/scripts/components/server/UptimeDuration.tsx b/resources/scripts/components/server/UptimeDuration.tsx
index 6623de0b8..e87700a61 100644
--- a/resources/scripts/components/server/UptimeDuration.tsx
+++ b/resources/scripts/components/server/UptimeDuration.tsx
@@ -1,5 +1,3 @@
-import React from 'react';
-
export default ({ uptime }: { uptime: number }) => {
const days = Math.floor(uptime / (24 * 60 * 60));
const hours = Math.floor((Math.floor(uptime) / 60 / 60) % 24);
diff --git a/resources/scripts/components/server/WebsocketHandler.tsx b/resources/scripts/components/server/WebsocketHandler.tsx
index 2772013df..954acda2c 100644
--- a/resources/scripts/components/server/WebsocketHandler.tsx
+++ b/resources/scripts/components/server/WebsocketHandler.tsx
@@ -1,29 +1,32 @@
-import React, { useEffect, useState } from 'react';
-import { Websocket } from '@/plugins/Websocket';
-import { ServerContext } from '@/state/server';
+import { useEffect, useState } from 'react';
+import tw from 'twin.macro';
+
import getWebsocketToken from '@/api/server/getWebsocketToken';
import ContentContainer from '@/components/elements/ContentContainer';
-import { CSSTransition } from 'react-transition-group';
import Spinner from '@/components/elements/Spinner';
-import tw from 'twin.macro';
+import FadeTransition from '@/components/elements/transitions/FadeTransition';
+import { Websocket } from '@/plugins/Websocket';
+import { ServerContext } from '@/state/server';
const reconnectErrors = ['jwt: exp claim is invalid', 'jwt: created too far in past (denylist)'];
-export default () => {
+function WebsocketHandler() {
let updatingToken = false;
const [error, setError] = useState<'connecting' | string>('');
- const { connected, instance } = ServerContext.useStoreState((state) => state.socket);
- const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
- const setServerStatus = ServerContext.useStoreActions((actions) => actions.status.setServerStatus);
- const { setInstance, setConnectionState } = ServerContext.useStoreActions((actions) => actions.socket);
+ const { connected, instance } = ServerContext.useStoreState(state => state.socket);
+ const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
+ const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus);
+ const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket);
const updateToken = (uuid: string, socket: Websocket) => {
- if (updatingToken) return;
+ if (updatingToken) {
+ return;
+ }
updatingToken = true;
getWebsocketToken(uuid)
- .then((data) => socket.setToken(data.token, true))
- .catch((error) => console.error(error))
+ .then(data => socket.setToken(data.token, true))
+ .catch(error => console.error(error))
.then(() => {
updatingToken = false;
});
@@ -38,9 +41,9 @@ export default () => {
setError('connecting');
setConnectionState(false);
});
- socket.on('status', (status) => setServerStatus(status));
+ socket.on('status', status => setServerStatus(status));
- socket.on('daemon error', (message) => {
+ socket.on('daemon error', message => {
console.warn('Got error message from daemon socket:', message);
});
@@ -50,11 +53,11 @@ export default () => {
setConnectionState(false);
console.warn('JWT validation error from wings:', error);
- if (reconnectErrors.find((v) => error.toLowerCase().indexOf(v) >= 0)) {
+ if (reconnectErrors.find(v => error.toLowerCase().indexOf(v) >= 0)) {
updateToken(uuid, socket);
} else {
setError(
- 'There was an error validating the credentials provided for the websocket. Please refresh the page.'
+ 'There was an error validating the credentials provided for the websocket. Please refresh the page.',
);
}
});
@@ -74,14 +77,14 @@ export default () => {
});
getWebsocketToken(uuid)
- .then((data) => {
+ .then(data => {
// Connect and then set the authentication token.
socket.setToken(data.token).connect(data.socket);
// Once that is done, set the instance.
setInstance(socket);
})
- .catch((error) => console.error(error));
+ .catch(error => console.error(error));
};
useEffect(() => {
@@ -105,7 +108,7 @@ export default () => {
}, [uuid]);
return error ? (
-
+
{error === 'connecting' ? (
@@ -120,6 +123,8 @@ export default () => {
)}
-
+
) : null;
-};
+}
+
+export default WebsocketHandler;
diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx
index 0134142bd..a980920c0 100644
--- a/resources/scripts/components/server/backups/BackupContainer.tsx
+++ b/resources/scripts/components/server/backups/BackupContainer.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from 'react';
+import { useContext, useEffect, useState } from 'react';
import Spinner from '@/components/elements/Spinner';
import useFlash from '@/plugins/useFlash';
import Can from '@/components/elements/Can';
@@ -16,7 +16,7 @@ const BackupContainer = () => {
const { clearFlashes, clearAndAddHttpError } = useFlash();
const { data: backups, error, isValidating } = getServerBackups();
- const backupLimit = ServerContext.useStoreState((state) => state.server.data!.featureLimits.backups);
+ const backupLimit = ServerContext.useStoreState(state => state.server.data!.featureLimits.backups);
useEffect(() => {
if (!error) {
diff --git a/resources/scripts/components/server/backups/BackupContextMenu.tsx b/resources/scripts/components/server/backups/BackupContextMenu.tsx
index 43c8b3575..4e371f2bd 100644
--- a/resources/scripts/components/server/backups/BackupContextMenu.tsx
+++ b/resources/scripts/components/server/backups/BackupContextMenu.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
import {
faBoxOpen,
faCloudDownloadAlt,
@@ -28,8 +28,8 @@ interface Props {
}
export default ({ backup }: Props) => {
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
- const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
+ const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
const [modal, setModal] = useState('');
const [loading, setLoading] = useState(false);
const [truncate, setTruncate] = useState(false);
@@ -40,11 +40,11 @@ export default ({ backup }: Props) => {
setLoading(true);
clearFlashes('backups');
getBackupDownloadUrl(uuid, backup.uuid)
- .then((url) => {
+ .then(url => {
// @ts-expect-error this is valid
window.location = url;
})
- .catch((error) => {
+ .catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'backups', error });
})
@@ -55,17 +55,18 @@ export default ({ backup }: Props) => {
setLoading(true);
clearFlashes('backups');
deleteBackup(uuid, backup.uuid)
- .then(() =>
- mutate(
- (data) => ({
- ...data,
- items: data.items.filter((b) => b.uuid !== backup.uuid),
- backupCount: data.backupCount - 1,
- }),
- false
- )
+ .then(
+ async () =>
+ await mutate(
+ data => ({
+ ...data!,
+ items: data!.items.filter(b => b.uuid !== backup.uuid),
+ backupCount: data!.backupCount - 1,
+ }),
+ false,
+ ),
)
- .catch((error) => {
+ .catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'backups', error });
setLoading(false);
@@ -78,12 +79,12 @@ export default ({ backup }: Props) => {
clearFlashes('backups');
restoreServerBackup(uuid, backup.uuid, truncate)
.then(() =>
- setServerFromState((s) => ({
+ setServerFromState(s => ({
...s,
status: 'restoring_backup',
- }))
+ })),
)
- .catch((error) => {
+ .catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'backups', error });
})
@@ -97,23 +98,24 @@ export default ({ backup }: Props) => {
}
http.post(`/api/client/servers/${uuid}/backups/${backup.uuid}/lock`)
- .then(() =>
- mutate(
- (data) => ({
- ...data,
- items: data.items.map((b) =>
- b.uuid !== backup.uuid
- ? b
- : {
- ...b,
- isLocked: !b.isLocked,
- }
- ),
- }),
- false
- )
+ .then(
+ async () =>
+ await mutate(
+ data => ({
+ ...data!,
+ items: data!.items.map(b =>
+ b.uuid !== backup.uuid
+ ? b
+ : {
+ ...b,
+ isLocked: !b.isLocked,
+ },
+ ),
+ }),
+ false,
+ ),
)
- .catch((error) => alert(httpErrorToHuman(error)))
+ .catch(error => alert(httpErrorToHuman(error)))
.then(() => setModal(''));
};
@@ -146,7 +148,7 @@ export default ({ backup }: Props) => {
id={'restore_truncate'}
value={'true'}
checked={truncate}
- onChange={() => setTruncate((s) => !s)}
+ onChange={() => setTruncate(s => !s)}
/>
Delete all files before restoring backup.
@@ -164,7 +166,7 @@ export default ({ backup }: Props) => {
{backup.isSuccessful ? (
(
+ renderToggle={onClick => (
{
const { mutate } = getServerBackups();
- useWebsocketEvent(`${SocketEvent.BACKUP_COMPLETED}:${backup.uuid}` as SocketEvent, (data) => {
+ useWebsocketEvent(`${SocketEvent.BACKUP_COMPLETED}:${backup.uuid}` as SocketEvent, async data => {
try {
const parsed = JSON.parse(data);
- mutate(
- (data) => ({
- ...data,
- items: data.items.map((b) =>
+ await mutate(
+ data => ({
+ ...data!,
+ items: data!.items.map(b =>
b.uuid !== backup.uuid
? b
: {
@@ -37,10 +36,10 @@ export default ({ backup, className }: Props) => {
checksum: (parsed.checksum_type || '') + ':' + (parsed.checksum || ''),
bytes: parsed.file_size || 0,
completedAt: new Date(),
- }
+ },
),
}),
- false
+ false,
);
} catch (e) {
console.warn(e);
diff --git a/resources/scripts/components/server/backups/CreateBackupButton.tsx b/resources/scripts/components/server/backups/CreateBackupButton.tsx
index fef0150c7..c98607538 100644
--- a/resources/scripts/components/server/backups/CreateBackupButton.tsx
+++ b/resources/scripts/components/server/backups/CreateBackupButton.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
import { Field as FormikField, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { boolean, object, string } from 'yup';
@@ -68,7 +68,7 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
};
export default () => {
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const { clearFlashes, clearAndAddHttpError } = useFlash();
const [visible, setVisible] = useState(false);
const { mutate } = getServerBackups();
@@ -80,14 +80,14 @@ export default () => {
const submit = (values: Values, { setSubmitting }: FormikHelpers) => {
clearFlashes('backups:create');
createServerBackup(uuid, values)
- .then((backup) => {
- mutate(
- (data) => ({ ...data, items: data.items.concat(backup), backupCount: data.backupCount + 1 }),
- false
+ .then(async backup => {
+ await mutate(
+ data => ({ ...data!, items: data!.items.concat(backup), backupCount: data!.backupCount + 1 }),
+ false,
);
setVisible(false);
})
- .catch((error) => {
+ .catch(error => {
clearAndAddHttpError({ key: 'backups:create', error });
setSubmitting(false);
});
diff --git a/resources/scripts/components/server/console/ChartBlock.tsx b/resources/scripts/components/server/console/ChartBlock.tsx
index cc55b02ca..5cc36c8cd 100644
--- a/resources/scripts/components/server/console/ChartBlock.tsx
+++ b/resources/scripts/components/server/console/ChartBlock.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import * as React from 'react';
import classNames from 'classnames';
import styles from '@/components/server/console/style.module.css';
diff --git a/resources/scripts/components/server/console/Console.tsx b/resources/scripts/components/server/console/Console.tsx
index e3cb43ab2..5c7beaa3f 100644
--- a/resources/scripts/components/server/console/Console.tsx
+++ b/resources/scripts/components/server/console/Console.tsx
@@ -1,25 +1,28 @@
-import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { ITerminalOptions, Terminal } from 'xterm';
+import { ChevronDoubleRightIcon } from '@heroicons/react/solid';
+import classNames from 'classnames';
+import { debounce } from 'debounce';
+import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import type { ITerminalInitOnlyOptions, ITerminalOptions, ITheme } from 'xterm';
+import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
import { SearchBarAddon } from 'xterm-addon-search-bar';
import { WebLinksAddon } from 'xterm-addon-web-links';
-import { ScrollDownHelperAddon } from '@/plugins/XtermScrollDownHelperAddon';
-import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
-import { ServerContext } from '@/state/server';
-import { usePermissions } from '@/plugins/usePermissions';
import { theme as th } from 'twin.macro';
-import useEventListener from '@/plugins/useEventListener';
-import { debounce } from 'debounce';
-import { usePersistedState } from '@/plugins/usePersistedState';
+
+import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { SocketEvent, SocketRequest } from '@/components/server/events';
-import classNames from 'classnames';
-import { ChevronDoubleRightIcon } from '@heroicons/react/solid';
+import { ScrollDownHelperAddon } from '@/plugins/XtermScrollDownHelperAddon';
+import useEventListener from '@/plugins/useEventListener';
+import { usePermissions } from '@/plugins/usePermissions';
+import { usePersistedState } from '@/plugins/usePersistedState';
+import { ServerContext } from '@/state/server';
import 'xterm/css/xterm.css';
import styles from './style.module.css';
-const theme = {
+const theme: ITheme = {
background: th`colors.black`.toString(),
cursor: 'transparent',
black: th`colors.black`.toString(),
@@ -38,7 +41,7 @@ const theme = {
brightMagenta: '#C792EA',
brightCyan: '#89DDFF',
brightWhite: '#ffffff',
- selection: '#FAF089',
+ selectionBackground: '#FAF089',
};
const terminalProps: ITerminalOptions = {
@@ -47,23 +50,27 @@ const terminalProps: ITerminalOptions = {
allowTransparency: true,
fontSize: 12,
fontFamily: th('fontFamily.mono'),
- rows: 30,
theme: theme,
+ allowProposedApi: true,
+};
+
+const terminalInitOnlyProps: ITerminalInitOnlyOptions = {
+ rows: 30,
};
export default () => {
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pterodactyl~ \u001b[0m';
const ref = useRef(null);
- const terminal = useMemo(() => new Terminal({ ...terminalProps }), []);
+ const terminal = useMemo(() => new Terminal({ ...terminalProps, ...terminalInitOnlyProps }), []);
const fitAddon = new FitAddon();
const searchAddon = new SearchAddon();
const searchBar = new SearchBarAddon({ searchAddon });
const webLinksAddon = new WebLinksAddon();
const scrollDownHelperAddon = new ScrollDownHelperAddon();
- const { connected, instance } = ServerContext.useStoreState((state) => state.socket);
+ const { connected, instance } = ServerContext.useStoreState(state => state.socket);
const [canSendCommands] = usePermissions(['control.console']);
- const serverId = ServerContext.useStoreState((state) => state.server.data!.id);
- const isTransferring = ServerContext.useStoreState((state) => state.server.data!.isTransferring);
+ const serverId = ServerContext.useStoreState(state => state.server.data!.id);
+ const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
const [history, setHistory] = usePersistedState(`${serverId}:command_history`, []);
const [historyIndex, setHistoryIndex] = useState(-1);
@@ -81,20 +88,20 @@ export default () => {
const handleDaemonErrorOutput = (line: string) =>
terminal.writeln(
- TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m'
+ TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
);
const handlePowerChangeEvent = (state: string) =>
terminal.writeln(TERMINAL_PRELUDE + 'Server marked as ' + state + '...\u001b[0m');
- const handleCommandKeyDown = (e: React.KeyboardEvent) => {
+ const handleCommandKeyDown = (e: ReactKeyboardEvent) => {
if (e.key === 'ArrowUp') {
const newIndex = Math.min(historyIndex + 1, history!.length - 1);
setHistoryIndex(newIndex);
e.currentTarget.value = history![newIndex] || '';
- // By default up arrow will also bring the cursor to the start of the line,
+ // By default, up arrow will also bring the cursor to the start of the line,
// so we'll preventDefault to keep it at the end.
e.preventDefault();
}
@@ -108,7 +115,7 @@ export default () => {
const command = e.currentTarget.value;
if (e.key === 'Enter' && command.length > 0) {
- setHistory((prevHistory) => [command, ...prevHistory!].slice(0, 32));
+ setHistory(prevHistory => [command, ...prevHistory!].slice(0, 32));
setHistoryIndex(-1);
instance && instance.send('send command', command);
@@ -150,7 +157,7 @@ export default () => {
if (terminal.element) {
fitAddon.fit();
}
- }, 100)
+ }, 100),
);
useEffect(() => {
@@ -160,7 +167,7 @@ export default () => {
[SocketEvent.INSTALL_OUTPUT]: handleConsoleOutput,
[SocketEvent.TRANSFER_LOGS]: handleConsoleOutput,
[SocketEvent.TRANSFER_STATUS]: handleTransferStatus,
- [SocketEvent.DAEMON_MESSAGE]: (line) => handleConsoleOutput(line, true),
+ [SocketEvent.DAEMON_MESSAGE]: line => handleConsoleOutput(line, true),
[SocketEvent.DAEMON_ERROR]: handleDaemonErrorOutput,
};
@@ -171,7 +178,12 @@ export default () => {
}
Object.keys(listeners).forEach((key: string) => {
- instance.addListener(key, listeners[key]);
+ const listener = listeners[key];
+ if (listener === undefined) {
+ return;
+ }
+
+ instance.addListener(key, listener);
});
instance.send(SocketRequest.SEND_LOGS);
}
@@ -179,7 +191,12 @@ export default () => {
return () => {
if (instance) {
Object.keys(listeners).forEach((key: string) => {
- instance.removeListener(key, listeners[key]);
+ const listener = listeners[key];
+ if (listener === undefined) {
+ return;
+ }
+
+ instance.removeListener(key, listener);
});
}
};
@@ -210,7 +227,7 @@ export default () => {
diff --git a/resources/scripts/components/server/console/PowerButtons.tsx b/resources/scripts/components/server/console/PowerButtons.tsx
index f5f81898f..446fada52 100644
--- a/resources/scripts/components/server/console/PowerButtons.tsx
+++ b/resources/scripts/components/server/console/PowerButtons.tsx
@@ -1,4 +1,5 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
+import * as React from 'react';
import { Button } from '@/components/elements/button/index';
import Can from '@/components/elements/Can';
import { ServerContext } from '@/state/server';
@@ -11,13 +12,13 @@ interface PowerButtonProps {
export default ({ className }: PowerButtonProps) => {
const [open, setOpen] = useState(false);
- const status = ServerContext.useStoreState((state) => state.status.value);
- const instance = ServerContext.useStoreState((state) => state.socket.instance);
+ const status = ServerContext.useStoreState(state => state.status.value);
+ const instance = ServerContext.useStoreState(state => state.socket.instance);
const killable = status === 'stopping';
const onButtonClick = (
action: PowerAction | 'kill-confirmed',
- e: React.MouseEvent
+ e: React.MouseEvent,
): void => {
e.preventDefault();
if (action === 'kill') {
diff --git a/resources/scripts/components/server/console/ServerConsoleContainer.tsx b/resources/scripts/components/server/console/ServerConsoleContainer.tsx
index c3af78f1c..6484f7e72 100644
--- a/resources/scripts/components/server/console/ServerConsoleContainer.tsx
+++ b/resources/scripts/components/server/console/ServerConsoleContainer.tsx
@@ -1,25 +1,26 @@
-import React, { memo } from 'react';
-import { ServerContext } from '@/state/server';
+import { memo } from 'react';
+import isEqual from 'react-fast-compare';
+
+import { Alert } from '@/components/elements/alert';
import Can from '@/components/elements/Can';
import ServerContentBlock from '@/components/elements/ServerContentBlock';
-import isEqual from 'react-fast-compare';
import Spinner from '@/components/elements/Spinner';
-import Features from '@feature/Features';
import Console from '@/components/server/console/Console';
-import StatGraphs from '@/components/server/console/StatGraphs';
import PowerButtons from '@/components/server/console/PowerButtons';
import ServerDetailsBlock from '@/components/server/console/ServerDetailsBlock';
-import { Alert } from '@/components/elements/alert';
+import StatGraphs from '@/components/server/console/StatGraphs';
+import Features from '@feature/Features';
+import { ServerContext } from '@/state/server';
export type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
-const ServerConsoleContainer = () => {
- const name = ServerContext.useStoreState((state) => state.server.data!.name);
- const description = ServerContext.useStoreState((state) => state.server.data!.description);
- const isInstalling = ServerContext.useStoreState((state) => state.server.isInstalling);
- const isTransferring = ServerContext.useStoreState((state) => state.server.data!.isTransferring);
- const eggFeatures = ServerContext.useStoreState((state) => state.server.data!.eggFeatures, isEqual);
- const isNodeUnderMaintenance = ServerContext.useStoreState((state) => state.server.data!.isNodeUnderMaintenance);
+function ServerConsoleContainer() {
+ const name = ServerContext.useStoreState(state => state.server.data!.name);
+ const description = ServerContext.useStoreState(state => state.server.data!.description);
+ const isInstalling = ServerContext.useStoreState(state => state.server.isInstalling);
+ const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
+ const eggFeatures = ServerContext.useStoreState(state => state.server.data!.eggFeatures, isEqual);
+ const isNodeUnderMaintenance = ServerContext.useStoreState(state => state.server.data!.isNodeUnderMaintenance);
return (
@@ -59,6 +60,6 @@ const ServerConsoleContainer = () => {
);
-};
+}
export default memo(ServerConsoleContainer, isEqual);
diff --git a/resources/scripts/components/server/console/ServerDetailsBlock.tsx b/resources/scripts/components/server/console/ServerDetailsBlock.tsx
index a3a009612..5e6dfb27f 100644
--- a/resources/scripts/components/server/console/ServerDetailsBlock.tsx
+++ b/resources/scripts/components/server/console/ServerDetailsBlock.tsx
@@ -1,4 +1,3 @@
-import React, { useEffect, useMemo, useState } from 'react';
import {
faClock,
faCloudDownloadAlt,
@@ -8,18 +7,21 @@ import {
faMicrochip,
faWifi,
} from '@fortawesome/free-solid-svg-icons';
-import { bytesToString, ip, mbToBytes } from '@/lib/formatters';
-import { ServerContext } from '@/state/server';
+import classNames from 'classnames';
+import type { ReactNode } from 'react';
+import { useEffect, useMemo, useState } from 'react';
+
import { SocketEvent, SocketRequest } from '@/components/server/events';
import UptimeDuration from '@/components/server/UptimeDuration';
import StatBlock from '@/components/server/console/StatBlock';
-import useWebsocketEvent from '@/plugins/useWebsocketEvent';
-import classNames from 'classnames';
+import { bytesToString, ip, mbToBytes } from '@/lib/formatters';
import { capitalize } from '@/lib/strings';
+import { ServerContext } from '@/state/server';
+import useWebsocketEvent from '@/plugins/useWebsocketEvent';
type Stats = Record<'memory' | 'cpu' | 'disk' | 'uptime' | 'rx' | 'tx', number>;
-const getBackgroundColor = (value: number, max: number | null): string | undefined => {
+function getBackgroundColor(value: number, max: number | null): string | undefined {
const delta = !max ? 0 : value / max;
if (delta > 0.8) {
@@ -30,22 +32,24 @@ const getBackgroundColor = (value: number, max: number | null): string | undefin
}
return undefined;
-};
+}
-const Limit = ({ limit, children }: { limit: string | null; children: React.ReactNode }) => (
- <>
- {children}
- / {limit || <>∞>}
- >
-);
+function Limit({ limit, children }: { limit: string | null; children: ReactNode }) {
+ return (
+ <>
+ {children}
+ / {limit || <>∞>}
+ >
+ );
+}
-const ServerDetailsBlock = ({ className }: { className?: string }) => {
+function ServerDetailsBlock({ className }: { className?: string }) {
const [stats, setStats] = useState({ memory: 0, cpu: 0, disk: 0, uptime: 0, tx: 0, rx: 0 });
- const status = ServerContext.useStoreState((state) => state.status.value);
- const connected = ServerContext.useStoreState((state) => state.socket.connected);
- const instance = ServerContext.useStoreState((state) => state.socket.instance);
- const limits = ServerContext.useStoreState((state) => state.server.data!.limits);
+ const status = ServerContext.useStoreState(state => state.status.value);
+ const connected = ServerContext.useStoreState(state => state.socket.connected);
+ const instance = ServerContext.useStoreState(state => state.socket.instance);
+ const limits = ServerContext.useStoreState(state => state.server.data!.limits);
const textLimits = useMemo(
() => ({
@@ -53,11 +57,11 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
memory: limits?.memory ? bytesToString(mbToBytes(limits.memory)) : null,
disk: limits?.disk ? bytesToString(mbToBytes(limits.disk)) : null,
}),
- [limits]
+ [limits],
);
- const allocation = ServerContext.useStoreState((state) => {
- const match = state.server.data!.allocations.find((allocation) => allocation.isDefault);
+ const allocation = ServerContext.useStoreState(state => {
+ const match = state.server.data!.allocations.find(allocation => allocation.isDefault);
return !match ? 'n/a' : `${match.alias || ip(match.ip)}:${match.port}`;
});
@@ -70,7 +74,7 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
instance.send(SocketRequest.SEND_STATS);
}, [instance, connected]);
- useWebsocketEvent(SocketEvent.STATS, (data) => {
+ useWebsocketEvent(SocketEvent.STATS, data => {
let stats: any = {};
try {
stats = JSON.parse(data);
@@ -135,6 +139,6 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
);
-};
+}
export default ServerDetailsBlock;
diff --git a/resources/scripts/components/server/console/StatBlock.tsx b/resources/scripts/components/server/console/StatBlock.tsx
index 7e4d04d20..7aa5df1c7 100644
--- a/resources/scripts/components/server/console/StatBlock.tsx
+++ b/resources/scripts/components/server/console/StatBlock.tsx
@@ -1,21 +1,23 @@
-import React from 'react';
-import Icon from '@/components/elements/Icon';
-import { IconDefinition } from '@fortawesome/free-solid-svg-icons';
+import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';
import classNames from 'classnames';
-import styles from './style.module.css';
-import useFitText from 'use-fit-text';
+import type { ReactNode } from 'react';
+import { useFitText } from '@flyyer/use-fit-text';
+
import CopyOnClick from '@/components/elements/CopyOnClick';
+import Icon from '@/components/elements/Icon';
+
+import styles from './style.module.css';
interface StatBlockProps {
title: string;
copyOnClick?: string;
color?: string | undefined;
icon: IconDefinition;
- children: React.ReactNode;
+ children: ReactNode;
className?: string;
}
-export default ({ title, copyOnClick, icon, color, className, children }: StatBlockProps) => {
+function StatBlock({ title, copyOnClick, icon, color, className, children }: StatBlockProps) {
const { fontSize, ref } = useFitText({ minFontSize: 8, maxFontSize: 500 });
return (
@@ -44,4 +46,6 @@ export default ({ title, copyOnClick, icon, color, className, children }: StatBl
);
-};
+}
+
+export default StatBlock;
diff --git a/resources/scripts/components/server/console/StatGraphs.tsx b/resources/scripts/components/server/console/StatGraphs.tsx
index 9af405ba6..a7ab28f45 100644
--- a/resources/scripts/components/server/console/StatGraphs.tsx
+++ b/resources/scripts/components/server/console/StatGraphs.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef } from 'react';
+import { useEffect, useRef } from 'react';
import { ServerContext } from '@/state/server';
import { SocketEvent } from '@/components/server/events';
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
@@ -12,8 +12,8 @@ import ChartBlock from '@/components/server/console/ChartBlock';
import Tooltip from '@/components/elements/tooltip/Tooltip';
export default () => {
- const status = ServerContext.useStoreState((state) => state.status.value);
- const limits = ServerContext.useStoreState((state) => state.server.data!.limits);
+ const status = ServerContext.useStoreState(state => state.status.value);
+ const limits = ServerContext.useStoreState(state => state.server.data!.limits);
const previous = useRef>({ tx: -1, rx: -1 });
const cpu = useChartTickLabel('CPU', limits.cpu, '%', 2);
diff --git a/resources/scripts/components/server/console/chart.ts b/resources/scripts/components/server/console/chart.ts
index 65f919aa1..d495cbbfc 100644
--- a/resources/scripts/components/server/console/chart.ts
+++ b/resources/scripts/components/server/console/chart.ts
@@ -71,13 +71,14 @@ const options: ChartOptions<'line'> = {
};
function getOptions(opts?: DeepPartial> | undefined): ChartOptions<'line'> {
- return deepmerge(options, opts || {});
+ // @ts-expect-error go away
+ return deepmerge(options, opts ?? {});
}
type ChartDatasetCallback = (value: ChartDataset<'line'>, index: number) => ChartDataset<'line'>;
function getEmptyData(label: string, sets = 1, callback?: ChartDatasetCallback | undefined): ChartData<'line'> {
- const next = callback || ((value) => value);
+ const next = callback || (value => value);
return {
labels: Array(20)
@@ -94,8 +95,8 @@ function getEmptyData(label: string, sets = 1, callback?: ChartDatasetCallback |
borderColor: theme('colors.cyan.400'),
backgroundColor: hexToRgba(theme('colors.cyan.700'), 0.5),
},
- index
- )
+ index,
+ ),
),
};
}
@@ -110,30 +111,31 @@ interface UseChartOptions {
function useChart(label: string, opts?: UseChartOptions) {
const options = getOptions(
- typeof opts?.options === 'number' ? { scales: { y: { min: 0, suggestedMax: opts.options } } } : opts?.options
+ typeof opts?.options === 'number' ? { scales: { y: { min: 0, suggestedMax: opts.options } } } : opts?.options,
);
const [data, setData] = useState(getEmptyData(label, opts?.sets || 1, opts?.callback));
const push = (items: number | null | (number | null)[]) =>
- setData((state) =>
+ setData(state =>
merge(state, {
datasets: (Array.isArray(items) ? items : [items]).map((item, index) => ({
...state.datasets[index],
- data: state.datasets[index].data
- .slice(1)
- .concat(typeof item === 'number' ? Number(item.toFixed(2)) : item),
+ data:
+ state.datasets[index]?.data
+ ?.slice(1)
+ ?.concat(typeof item === 'number' ? Number(item.toFixed(2)) : item) ?? [],
})),
- })
+ }),
);
const clear = () =>
- setData((state) =>
+ setData(state =>
merge(state, {
- datasets: state.datasets.map((value) => ({
+ datasets: state.datasets.map(value => ({
...value,
data: Array(20).fill(-5),
})),
- })
+ }),
);
return { props: { data, options }, push, clear };
diff --git a/resources/scripts/components/server/databases/CreateDatabaseButton.tsx b/resources/scripts/components/server/databases/CreateDatabaseButton.tsx
index 24c19e040..a3c85e288 100644
--- a/resources/scripts/components/server/databases/CreateDatabaseButton.tsx
+++ b/resources/scripts/components/server/databases/CreateDatabaseButton.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
import Modal from '@/components/elements/Modal';
import { Form, Formik, FormikHelpers } from 'formik';
import Field from '@/components/elements/Field';
@@ -23,17 +23,17 @@ const schema = object().shape({
.max(48, 'Database name must not exceed 48 characters.')
.matches(
/^[\w\-.]{3,48}$/,
- 'Database name should only contain alphanumeric characters, underscores, dashes, and/or periods.'
+ 'Database name should only contain alphanumeric characters, underscores, dashes, and/or periods.',
),
connectionsFrom: string().matches(/^[\w\-/.%:]+$/, 'A valid host address must be provided.'),
});
export default () => {
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const { addError, clearFlashes } = useFlash();
const [visible, setVisible] = useState(false);
- const appendDatabase = ServerContext.useStoreActions((actions) => actions.databases.appendDatabase);
+ const appendDatabase = ServerContext.useStoreActions(actions => actions.databases.appendDatabase);
const submit = (values: Values, { setSubmitting }: FormikHelpers) => {
clearFlashes('database:create');
@@ -41,11 +41,11 @@ export default () => {
databaseName: values.databaseName,
connectionsFrom: values.connectionsFrom || '%',
})
- .then((database) => {
+ .then(database => {
appendDatabase(database);
setVisible(false);
})
- .catch((error) => {
+ .catch(error => {
addError({ key: 'database:create', message: httpErrorToHuman(error) });
setSubmitting(false);
});
diff --git a/resources/scripts/components/server/databases/DatabaseRow.tsx b/resources/scripts/components/server/databases/DatabaseRow.tsx
index 9d2c05a73..32c9f6659 100644
--- a/resources/scripts/components/server/databases/DatabaseRow.tsx
+++ b/resources/scripts/components/server/databases/DatabaseRow.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDatabase, faEye, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import Modal from '@/components/elements/Modal';
@@ -26,13 +26,13 @@ interface Props {
}
export default ({ database, className }: Props) => {
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const { addError, clearFlashes } = useFlash();
const [visible, setVisible] = useState(false);
const [connectionVisible, setConnectionVisible] = useState(false);
- const appendDatabase = ServerContext.useStoreActions((actions) => actions.databases.appendDatabase);
- const removeDatabase = ServerContext.useStoreActions((actions) => actions.databases.removeDatabase);
+ const appendDatabase = ServerContext.useStoreActions(actions => actions.databases.appendDatabase);
+ const removeDatabase = ServerContext.useStoreActions(actions => actions.databases.removeDatabase);
const jdbcConnectionString = `jdbc:mysql://${database.username}${
database.password ? `:${encodeURIComponent(database.password)}` : ''
@@ -44,14 +44,14 @@ export default ({ database, className }: Props) => {
.oneOf([database.name.split('_', 2)[1], database.name], 'The database name must be provided.'),
});
- const submit = (values: { confirm: string }, { setSubmitting }: FormikHelpers<{ confirm: string }>) => {
+ const submit = (_: { confirm: string }, { setSubmitting }: FormikHelpers<{ confirm: string }>) => {
clearFlashes();
deleteServerDatabase(uuid, database.id)
.then(() => {
setVisible(false);
setTimeout(() => removeDatabase(database.id), 150);
})
- .catch((error) => {
+ .catch(error => {
console.error(error);
setSubmitting(false);
addError({ key: 'database:delete', message: httpErrorToHuman(error) });
diff --git a/resources/scripts/components/server/databases/DatabasesContainer.tsx b/resources/scripts/components/server/databases/DatabasesContainer.tsx
index 16b690014..6d74bb965 100644
--- a/resources/scripts/components/server/databases/DatabasesContainer.tsx
+++ b/resources/scripts/components/server/databases/DatabasesContainer.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import getServerDatabases from '@/api/server/databases/getServerDatabases';
import { ServerContext } from '@/state/server';
import { httpErrorToHuman } from '@/api/http';
@@ -9,27 +9,27 @@ import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseBu
import Can from '@/components/elements/Can';
import useFlash from '@/plugins/useFlash';
import tw from 'twin.macro';
-import Fade from '@/components/elements/Fade';
import ServerContentBlock from '@/components/elements/ServerContentBlock';
import { useDeepMemoize } from '@/plugins/useDeepMemoize';
+import FadeTransition from '@/components/elements/transitions/FadeTransition';
export default () => {
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
- const databaseLimit = ServerContext.useStoreState((state) => state.server.data!.featureLimits.databases);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
+ const databaseLimit = ServerContext.useStoreState(state => state.server.data!.featureLimits.databases);
const { addError, clearFlashes } = useFlash();
const [loading, setLoading] = useState(true);
- const databases = useDeepMemoize(ServerContext.useStoreState((state) => state.databases.data));
- const setDatabases = ServerContext.useStoreActions((state) => state.databases.setDatabases);
+ const databases = useDeepMemoize(ServerContext.useStoreState(state => state.databases.data));
+ const setDatabases = ServerContext.useStoreActions(state => state.databases.setDatabases);
useEffect(() => {
setLoading(!databases.length);
clearFlashes('databases');
getServerDatabases(uuid)
- .then((databases) => setDatabases(databases))
- .catch((error) => {
+ .then(databases => setDatabases(databases))
+ .catch(error => {
console.error(error);
addError({ key: 'databases', message: httpErrorToHuman(error) });
})
@@ -42,7 +42,7 @@ export default () => {
{!databases.length && loading ? (
) : (
-
+
<>
{databases.length > 0 ? (
databases.map((database, index) => (
@@ -73,7 +73,7 @@ export default () => {
>
-
+
)}
);
diff --git a/resources/scripts/components/server/databases/RotatePasswordButton.tsx b/resources/scripts/components/server/databases/RotatePasswordButton.tsx
index 0c687d776..1159dd87b 100644
--- a/resources/scripts/components/server/databases/RotatePasswordButton.tsx
+++ b/resources/scripts/components/server/databases/RotatePasswordButton.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
import rotateDatabasePassword from '@/api/server/databases/rotateDatabasePassword';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
@@ -11,7 +11,7 @@ import tw from 'twin.macro';
export default ({ databaseId, onUpdate }: { databaseId: string; onUpdate: (database: ServerDatabase) => void }) => {
const [loading, setLoading] = useState(false);
const { addFlash, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes);
- const server = ServerContext.useStoreState((state) => state.server.data!);
+ const server = ServerContext.useStoreState(state => state.server.data!);
if (!databaseId) {
return null;
@@ -22,8 +22,8 @@ export default ({ databaseId, onUpdate }: { databaseId: string; onUpdate: (datab
clearFlashes();
rotateDatabasePassword(server.uuid, databaseId)
- .then((database) => onUpdate(database))
- .catch((error) => {
+ .then(database => onUpdate(database))
+ .catch(error => {
console.error(error);
addFlash({
type: 'error',
diff --git a/resources/scripts/components/server/features/Features.tsx b/resources/scripts/components/server/features/Features.tsx
index 571c7afc6..720b18853 100644
--- a/resources/scripts/components/server/features/Features.tsx
+++ b/resources/scripts/components/server/features/Features.tsx
@@ -1,21 +1,23 @@
-import React, { useMemo } from 'react';
+import type { ComponentType } from 'react';
+import { Suspense, useMemo } from 'react';
+
import features from './index';
import { getObjectKeys } from '@/lib/objects';
-type ListItems = [string, React.ComponentType][];
+type ListItems = [string, ComponentType][];
export default ({ enabled }: { enabled: string[] }) => {
const mapped: ListItems = useMemo(() => {
return getObjectKeys(features)
- .filter((key) => enabled.map((v) => v.toLowerCase()).includes(key.toLowerCase()))
- .reduce((arr, key) => [...arr, [key, features[key]]], [] as ListItems);
+ .filter(key => enabled.map(v => v.toLowerCase()).includes(key.toLowerCase()))
+ .reduce((arr, key) => [...arr, [key, features[key]]] as ListItems, [] as ListItems);
}, [enabled]);
return (
-
+
{mapped.map(([key, Component]) => (
))}
-
+
);
};
diff --git a/resources/scripts/components/server/features/GSLTokenModalFeature.tsx b/resources/scripts/components/server/features/GSLTokenModalFeature.tsx
index f56ae7cca..ec2eead4a 100644
--- a/resources/scripts/components/server/features/GSLTokenModalFeature.tsx
+++ b/resources/scripts/components/server/features/GSLTokenModalFeature.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import Modal from '@/components/elements/Modal';
import tw from 'twin.macro';
@@ -18,10 +18,10 @@ const GSLTokenModalFeature = () => {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
- const status = ServerContext.useStoreState((state) => state.status.value);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
+ const status = ServerContext.useStoreState(state => state.status.value);
const { clearFlashes, clearAndAddHttpError } = useFlash();
- const { connected, instance } = ServerContext.useStoreState((state) => state.socket);
+ const { connected, instance } = ServerContext.useStoreState(state => state.socket);
useEffect(() => {
if (!connected || !instance || status === 'running') return;
@@ -29,7 +29,7 @@ const GSLTokenModalFeature = () => {
const errors = ['(gsl token expired)', '(account not found)'];
const listener = (line: string) => {
- if (errors.some((p) => line.toLowerCase().includes(p))) {
+ if (errors.some(p => line.toLowerCase().includes(p))) {
setVisible(true);
}
};
@@ -54,7 +54,7 @@ const GSLTokenModalFeature = () => {
setLoading(false);
setVisible(false);
})
- .catch((error) => {
+ .catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'feature:gslToken', error });
})
diff --git a/resources/scripts/components/server/features/JavaVersionModalFeature.tsx b/resources/scripts/components/server/features/JavaVersionModalFeature.tsx
index 91902f0aa..80d8cc287 100644
--- a/resources/scripts/components/server/features/JavaVersionModalFeature.tsx
+++ b/resources/scripts/components/server/features/JavaVersionModalFeature.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import Modal from '@/components/elements/Modal';
import tw from 'twin.macro';
@@ -26,25 +26,25 @@ const JavaVersionModalFeature = () => {
const [loading, setLoading] = useState(false);
const [selectedVersion, setSelectedVersion] = useState('');
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
- const status = ServerContext.useStoreState((state) => state.status.value);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
+ const status = ServerContext.useStoreState(state => state.status.value);
const { clearFlashes, clearAndAddHttpError } = useFlash();
- const { instance } = ServerContext.useStoreState((state) => state.socket);
+ const { instance } = ServerContext.useStoreState(state => state.socket);
- const { data, isValidating, mutate } = getServerStartup(uuid, null, { revalidateOnMount: false });
+ const { data, isValidating, mutate } = getServerStartup(uuid, undefined, { revalidateOnMount: false });
useEffect(() => {
if (!visible) return;
- mutate().then((value) => {
+ mutate().then(value => {
setSelectedVersion(Object.values(value?.dockerImages || [])[0] || '');
});
}, [visible]);
- useWebsocketEvent(SocketEvent.CONSOLE_OUTPUT, (data) => {
+ useWebsocketEvent(SocketEvent.CONSOLE_OUTPUT, data => {
if (status === 'running') return;
- if (MATCH_ERRORS.some((p) => data.toLowerCase().includes(p.toLowerCase()))) {
+ if (MATCH_ERRORS.some(p => data.toLowerCase().includes(p.toLowerCase()))) {
setVisible(true);
}
});
@@ -60,7 +60,7 @@ const JavaVersionModalFeature = () => {
}
setVisible(false);
})
- .catch((error) => clearAndAddHttpError({ key: 'feature:javaVersion', error }))
+ .catch(error => clearAndAddHttpError({ key: 'feature:javaVersion', error }))
.then(() => setLoading(false));
};
@@ -86,11 +86,11 @@ const JavaVersionModalFeature = () => {
- setSelectedVersion(e.target.value)}>
+ setSelectedVersion(e.target.value)}>
{!data ? (
) : (
- Object.keys(data.dockerImages).map((key) => (
+ Object.keys(data.dockerImages).map(key => (
{key}
diff --git a/resources/scripts/components/server/features/PIDLimitModalFeature.tsx b/resources/scripts/components/server/features/PIDLimitModalFeature.tsx
index f4061bc6d..054f6aaf2 100644
--- a/resources/scripts/components/server/features/PIDLimitModalFeature.tsx
+++ b/resources/scripts/components/server/features/PIDLimitModalFeature.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import Modal from '@/components/elements/Modal';
import tw from 'twin.macro';
@@ -14,10 +14,10 @@ const PIDLimitModalFeature = () => {
const [visible, setVisible] = useState(false);
const [loading] = useState(false);
- const status = ServerContext.useStoreState((state) => state.status.value);
+ const status = ServerContext.useStoreState(state => state.status.value);
const { clearFlashes } = useFlash();
- const { connected, instance } = ServerContext.useStoreState((state) => state.socket);
- const isAdmin = useStoreState((state) => state.user.data!.rootAdmin);
+ const { connected, instance } = ServerContext.useStoreState(state => state.socket);
+ const isAdmin = useStoreState(state => state.user.data!.rootAdmin);
useEffect(() => {
if (!connected || !instance || status === 'running') return;
@@ -32,7 +32,7 @@ const PIDLimitModalFeature = () => {
];
const listener = (line: string) => {
- if (errors.some((p) => line.toLowerCase().includes(p))) {
+ if (errors.some(p => line.toLowerCase().includes(p))) {
setVisible(true);
}
};
diff --git a/resources/scripts/components/server/features/SteamDiskSpaceFeature.tsx b/resources/scripts/components/server/features/SteamDiskSpaceFeature.tsx
index 81acba579..36aa74563 100644
--- a/resources/scripts/components/server/features/SteamDiskSpaceFeature.tsx
+++ b/resources/scripts/components/server/features/SteamDiskSpaceFeature.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import Modal from '@/components/elements/Modal';
import tw from 'twin.macro';
@@ -12,10 +12,10 @@ const SteamDiskSpaceFeature = () => {
const [visible, setVisible] = useState(false);
const [loading] = useState(false);
- const status = ServerContext.useStoreState((state) => state.status.value);
+ const status = ServerContext.useStoreState(state => state.status.value);
const { clearFlashes } = useFlash();
- const { connected, instance } = ServerContext.useStoreState((state) => state.socket);
- const isAdmin = useStoreState((state) => state.user.data!.rootAdmin);
+ const { connected, instance } = ServerContext.useStoreState(state => state.socket);
+ const isAdmin = useStoreState(state => state.user.data!.rootAdmin);
useEffect(() => {
if (!connected || !instance || status === 'running') return;
@@ -23,7 +23,7 @@ const SteamDiskSpaceFeature = () => {
const errors = ['steamcmd needs 250mb of free disk space to update', '0x202 after update job'];
const listener = (line: string) => {
- if (errors.some((p) => line.toLowerCase().includes(p))) {
+ if (errors.some(p => line.toLowerCase().includes(p))) {
setVisible(true);
}
};
diff --git a/resources/scripts/components/server/features/eula/EulaModalFeature.tsx b/resources/scripts/components/server/features/eula/EulaModalFeature.tsx
index fd7afe1d4..6fa3ae6ce 100644
--- a/resources/scripts/components/server/features/eula/EulaModalFeature.tsx
+++ b/resources/scripts/components/server/features/eula/EulaModalFeature.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import Modal from '@/components/elements/Modal';
import tw from 'twin.macro';
@@ -12,10 +12,10 @@ const EulaModalFeature = () => {
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
- const status = ServerContext.useStoreState((state) => state.status.value);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
+ const status = ServerContext.useStoreState(state => state.status.value);
const { clearFlashes, clearAndAddHttpError } = useFlash();
- const { connected, instance } = ServerContext.useStoreState((state) => state.socket);
+ const { connected, instance } = ServerContext.useStoreState(state => state.socket);
useEffect(() => {
if (!connected || !instance || status === 'running') return;
@@ -46,7 +46,7 @@ const EulaModalFeature = () => {
setLoading(false);
setVisible(false);
})
- .catch((error) => {
+ .catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'feature:eula', error });
})
@@ -72,7 +72,7 @@ const EulaModalFeature = () => {
target={'_blank'}
css={tw`text-primary-300 underline transition-colors duration-150 hover:text-primary-400`}
rel={'noreferrer noopener'}
- href='https://account.mojang.com/documents/minecraft_eula'
+ href="https://account.mojang.com/documents/minecraft_eula"
>
Minecraft® EULA
diff --git a/resources/scripts/components/server/files/ChmodFileModal.tsx b/resources/scripts/components/server/files/ChmodFileModal.tsx
index 27a474d93..7a0e2fa42 100644
--- a/resources/scripts/components/server/files/ChmodFileModal.tsx
+++ b/resources/scripts/components/server/files/ChmodFileModal.tsx
@@ -1,6 +1,5 @@
import { fileBitsToString } from '@/helpers';
import useFileManagerSwr from '@/plugins/useFileManagerSwr';
-import React from 'react';
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
import { Form, Formik, FormikHelpers } from 'formik';
import Field from '@/components/elements/Field';
@@ -22,29 +21,29 @@ interface File {
type OwnProps = RequiredModalProps & { files: File[] };
const ChmodFileModal = ({ files, ...props }: OwnProps) => {
- const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
+ const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const { mutate } = useFileManagerSwr();
const { clearFlashes, clearAndAddHttpError } = useFlash();
- const directory = ServerContext.useStoreState((state) => state.files.directory);
- const setSelectedFiles = ServerContext.useStoreActions((actions) => actions.files.setSelectedFiles);
+ const directory = ServerContext.useStoreState(state => state.files.directory);
+ const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles);
- const submit = ({ mode }: FormikValues, { setSubmitting }: FormikHelpers) => {
+ const submit = async ({ mode }: FormikValues, { setSubmitting }: FormikHelpers) => {
clearFlashes('files');
- mutate(
- (data) =>
- data.map((f) =>
- f.name === files[0].file ? { ...f, mode: fileBitsToString(mode, !f.isFile), modeBits: mode } : f
+ await mutate(
+ data =>
+ data!.map(f =>
+ f.name === files[0]?.file ? { ...f, mode: fileBitsToString(mode, !f.isFile), modeBits: mode } : f,
),
- false
+ false,
);
- const data = files.map((f) => ({ file: f.file, mode: mode }));
+ const data = files.map(f => ({ file: f.file, mode: mode }));
chmodFiles(uuid, directory, data)
.then((): Promise => (files.length > 0 ? mutate() : Promise.resolve()))
.then(() => setSelectedFiles([]))
- .catch((error) => {
+ .catch(error => {
mutate();
setSubmitting(false);
clearAndAddHttpError({ key: 'files', error });
@@ -53,7 +52,7 @@ const ChmodFileModal = ({ files, ...props }: OwnProps) => {
};
return (
- 1 ? '' : files[0].mode || '' }}>
+ 1 ? '' : files[0]?.mode ?? '' }}>
{({ isSubmitting }) => (