diff --git a/app/Http/Controllers/Api/Application/Nodes/NodeController.php b/app/Http/Controllers/Api/Application/Nodes/NodeController.php index 4017926e..f2e5e547 100644 --- a/app/Http/Controllers/Api/Application/Nodes/NodeController.php +++ b/app/Http/Controllers/Api/Application/Nodes/NodeController.php @@ -56,8 +56,8 @@ class NodeController extends ApplicationApiController } $nodes = QueryBuilder::for(Node::query()) - ->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id']) - ->allowedSorts(['id', 'uuid', 'memory', 'disk']) + ->allowedFilters(['id', 'uuid', 'name', 'fqdn', 'daemon_token_id']) + ->allowedSorts(['id', 'uuid', 'name', 'location_id', 'fqdn', 'memory', 'disk']) ->paginate($perPage); return $this->fractal->collection($nodes) diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index eeb8aa55..b09cf613 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -48,7 +48,7 @@ class ServerController extends ApplicationApiController $servers = QueryBuilder::for(Server::query()) ->allowedFilters(['uuid', 'name', 'image', 'external_id']) - ->allowedSorts(['id', 'uuid', 'owner_id', 'node_id', 'status']) + ->allowedSorts(['id', 'uuid', 'uuidShort', 'name', 'owner_id', 'node_id', 'status']) ->paginate($perPage); return $this->fractal->collection($servers) diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index 71a7fc94..b5b3d7ea 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -19,7 +19,7 @@ use Pterodactyl\Http\Requests\Api\Application\Users\DeleteUserRequest; use Pterodactyl\Http\Requests\Api\Application\Users\UpdateUserRequest; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; -class UserController extends ApplicationApiController +class UserController extends ApplicationApiController { private UserRepositoryInterface $repository; private UserCreationService $creationService; diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index 622a78bb..47b1e640 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -60,8 +60,6 @@ class ServerTransformer extends BaseTransformer 'name' => $model->name, 'description' => $model->description, 'status' => $model->status, - // This field is deprecated, please use "status". - 'suspended' => $model->isSuspended(), 'limits' => [ 'memory' => $model->memory, 'swap' => $model->swap, @@ -75,16 +73,14 @@ class ServerTransformer extends BaseTransformer 'allocations' => $model->allocation_limit, 'backups' => $model->backup_limit, ], - 'user' => $model->owner_id, - 'node' => $model->node_id, - 'allocation' => $model->allocation_id, - 'nest' => $model->nest_id, - 'egg' => $model->egg_id, + 'owner_id' => $model->owner_id, + 'node_id' => $model->node_id, + 'allocation_id' => $model->allocation_id, + 'nest_id' => $model->nest_id, + 'egg_id' => $model->egg_id, 'container' => [ 'startup_command' => $model->startup, 'image' => $model->image, - // This field is deprecated, please use "status". - 'installed' => $model->isInstalled() ? 1 : 0, 'environment' => $this->environmentService->handle($model), ], $model->getUpdatedAtColumn() => $this->formatTimestamp($model->updated_at), diff --git a/package.json b/package.json index fd95c001..c90de956 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,25 @@ { "name": "pterodactyl-panel", "dependencies": { + "@codemirror/autocomplete": "^0.18.5", + "@codemirror/closebrackets": "^0.18.0", + "@codemirror/commands": "^0.18.2", + "@codemirror/comment": "^0.18.1", + "@codemirror/fold": "^0.18.1", + "@codemirror/gutter": "^0.18.3", + "@codemirror/highlight": "^0.18.4", + "@codemirror/history": "^0.18.1", + "@codemirror/lang-json": "^0.18.0", + "@codemirror/language": "^0.18.1", + "@codemirror/legacy-modes": "^0.18.0", + "@codemirror/lint": "^0.18.3", + "@codemirror/matchbrackets": "^0.18.0", + "@codemirror/rectangular-selection": "^0.18.0", + "@codemirror/search": "^0.18.3", + "@codemirror/state": "^0.18.7", + "@codemirror/stream-parser": "^0.18.2", + "@codemirror/theme-one-dark": "^0.18.1", + "@codemirror/view": "^0.18.12", "@fortawesome/fontawesome-svg-core": "^1.2.34", "@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/react-fontawesome": "^0.1.14", @@ -34,16 +53,16 @@ "sockette": "^2.0.6", "styled-components": "^5.2.1", "styled-components-breakpoint": "^3.0.0-preview.20", - "swr": "^0.2.3", + "swr": "^0.5.6", "tailwindcss": "^2.0.2", "uuid": "^3.3.2", - "xterm": "^4.10.0", + "xterm": "^4.12.0", "xterm-addon-attach": "^0.6.0", "xterm-addon-fit": "^0.5.0", "xterm-addon-search": "^0.8.0", "xterm-addon-search-bar": "^0.2.0", "xterm-addon-web-links": "^0.4.0", - "yup": "^0.29.1" + "yup": "^0.32.9" }, "devDependencies": { "@babel/core": "^7.12.1", diff --git a/resources/scripts/api/admin/nodes/getAllocations.ts b/resources/scripts/api/admin/nodes/getAllocations.ts new file mode 100644 index 00000000..0cb561b7 --- /dev/null +++ b/resources/scripts/api/admin/nodes/getAllocations.ts @@ -0,0 +1,23 @@ +import http from '@/api/http'; +import { rawDataToServerAllocation } from '@/api/transformers'; + +export interface Allocation { + id: number; + ip: string; + alias: string | null; + port: number; + notes: string | null; + isDefault: boolean; +} + +export default (uuid: string): Promise<[ Allocation, string[] ]> => { + return new Promise((resolve, reject) => { + http.get(`/api/application/allocations/${uuid}`) + .then(({ data }) => resolve([ + rawDataToServerAllocation(data), + // eslint-disable-next-line camelcase + data.meta?.is_allocation_owner ? [ '*' ] : (data.meta?.user_permissions || []), + ])) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/admin/nodes/getNodes.ts b/resources/scripts/api/admin/nodes/getNodes.ts index 5d70dee4..ddddbfea 100644 --- a/resources/scripts/api/admin/nodes/getNodes.ts +++ b/resources/scripts/api/admin/nodes/getNodes.ts @@ -67,18 +67,61 @@ export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({ }, }); +export interface Filters { + uuid?: string; + name?: string; + image?: string; + /* eslint-disable camelcase */ + external_id?: string; + /* eslint-enable camelcase */ +} + interface ctx { page: number; setPage: (value: number | ((s: number) => number)) => void; + + filters: Filters | null; + setFilters: (filters: Filters | null) => void; + + sort: string | null; + setSort: (sort: string | null) => void; + + sortDirection: boolean; + setSortDirection: (direction: boolean) => void; } -export const Context = createContext({ page: 1, setPage: () => 1 }); +export const Context = createContext({ + page: 1, + setPage: () => 1, + + filters: null, + setFilters: () => null, + + sort: null, + setSort: () => null, + + sortDirection: false, + setSortDirection: () => false, +}); export default (include: string[] = []) => { - const { page } = useContext(Context); + const { page, filters, sort, sortDirection } = useContext(Context); - return useSWR>([ 'nodes', page ], async () => { - const { data } = await http.get('/api/application/nodes', { params: { include: include.join(','), page } }); + const params = {}; + if (filters !== null) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; + }); + } + + if (sort !== null) { + // @ts-ignore + params.sort = (sortDirection ? '-' : '') + sort; + } + + return useSWR>([ 'nodes', page, filters, sort, sortDirection ], async () => { + const { data } = await http.get('/api/application/nodes', { params: { include: include.join(','), page, ...params } }); return ({ items: (data.data || []).map(rawDataToNode), diff --git a/resources/scripts/api/admin/servers/getServers.ts b/resources/scripts/api/admin/servers/getServers.ts index 121f5682..fccaef91 100644 --- a/resources/scripts/api/admin/servers/getServers.ts +++ b/resources/scripts/api/admin/servers/getServers.ts @@ -1,3 +1,4 @@ +import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg'; import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; import { createContext, useContext } from 'react'; import useSWR from 'swr'; @@ -6,18 +7,46 @@ import { User, rawDataToUser } from '@/api/admin/users/getUsers'; export interface Server { id: number; - externalId: string; + externalId: string | null uuid: string; identifier: string; name: string; description: string; - isSuspended: boolean; - isInstalling: boolean; - isTransferring: boolean; + status: string; + + limits: { + memory: number; + swap: number; + disk: number; + io: number; + cpu: number; + threads: string; + } + + featureLimits: { + databases: number; + allocations: number; + backups: number; + } + + ownerId: number; + nodeId: number; + allocationId: number; + nestId: number; + eggId: number; + + container: { + startupCommand: string; + defaultStartup: string; + image: string; + environment: Map; + } + createdAt: Date; updatedAt: Date; relations: { + egg: Egg | undefined; node: Node | undefined; user: User | undefined; }; @@ -30,13 +59,41 @@ export const rawDataToServer = ({ attributes }: FractalResponseData): Server => identifier: attributes.identifier, name: attributes.name, description: attributes.description, - isSuspended: attributes.is_suspended, - isInstalling: attributes.is_installing, - isTransferring: attributes.is_transferring, + status: attributes.status, + + limits: { + memory: attributes.limits.memory, + swap: attributes.limits.swap, + disk: attributes.limits.disk, + io: attributes.limits.io, + cpu: attributes.limits.cpu, + threads: attributes.limits.threads, + }, + + featureLimits: { + databases: attributes.feature_limits.databases, + allocations: attributes.feature_limits.allocations, + backups: attributes.feature_limits.backups, + }, + + ownerId: attributes.owner_id, + nodeId: attributes.node_id, + allocationId: attributes.allocation_id, + nestId: attributes.nest_id, + eggId: attributes.egg_id, + + container: { + startupCommand: attributes.container.startup_command, + defaultStartup: '', + image: attributes.container.image, + environment: attributes.container.environment, + }, + createdAt: new Date(attributes.created_at), updatedAt: new Date(attributes.updated_at), relations: { + egg: attributes.relationships?.egg !== undefined ? rawDataToEgg(attributes.relationships.egg as FractalResponseData) : undefined, node: attributes.relationships?.node !== undefined ? rawDataToNode(attributes.relationships.node as FractalResponseData) : undefined, user: attributes.relationships?.user !== undefined ? rawDataToUser(attributes.relationships.user as FractalResponseData) : undefined, }, diff --git a/resources/scripts/api/admin/servers/updateServer.ts b/resources/scripts/api/admin/servers/updateServer.ts new file mode 100644 index 00000000..d02870d8 --- /dev/null +++ b/resources/scripts/api/admin/servers/updateServer.ts @@ -0,0 +1,12 @@ +import http from '@/api/http'; +import { Server, rawDataToServer } from '@/api/admin/servers/getServers'; + +export default (id: number, server: Partial, include: string[] = []): Promise => { + return new Promise((resolve, reject) => { + http.patch(`/api/application/servers/${id}`, { + ...server, + }, { params: { include: include.join(',') } }) + .then(({ data }) => resolve(rawDataToServer(data))) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/admin/users/searchUsers.ts b/resources/scripts/api/admin/users/searchUsers.ts new file mode 100644 index 00000000..450ca436 --- /dev/null +++ b/resources/scripts/api/admin/users/searchUsers.ts @@ -0,0 +1,25 @@ +import http from '@/api/http'; +import { User, rawDataToUser } from '@/api/admin/users/getUsers'; + +interface Filters { + username?: string; + email?: string; +} + +export default (filters?: Filters): Promise => { + const params = {}; + if (filters !== undefined) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; + }); + } + + return new Promise((resolve, reject) => { + http.get('/api/application/users', { params: { ...params } }) + .then(response => resolve( + (response.data.data || []).map(rawDataToUser) + )) + .catch(reject); + }); +}; diff --git a/resources/scripts/components/admin/AdminBox.tsx b/resources/scripts/components/admin/AdminBox.tsx index 9519dbdc..12d53c3c 100644 --- a/resources/scripts/components/admin/AdminBox.tsx +++ b/resources/scripts/components/admin/AdminBox.tsx @@ -8,24 +8,31 @@ interface Props { icon?: IconProp; title: string | React.ReactNode; className?: string; + padding?: boolean; children: React.ReactNode; } -const AdminBox = ({ icon, title, children, className }: Props) => ( -
-
- {typeof title === 'string' ? -

- {icon && }{title} -

- : - title - } +const AdminBox = ({ icon, title, className, padding, children }: Props) => { + if (padding === undefined) { + padding = true; + } + + return ( +
+
+ {typeof title === 'string' ? +

+ {icon && }{title} +

+ : + title + } +
+
+ {children} +
-
- {children} -
-
-); + ); +}; export default memo(AdminBox, isEqual); diff --git a/resources/scripts/components/admin/locations/LocationEditContainer.tsx b/resources/scripts/components/admin/locations/LocationEditContainer.tsx index 36457a83..76cf5c62 100644 --- a/resources/scripts/components/admin/locations/LocationEditContainer.tsx +++ b/resources/scripts/components/admin/locations/LocationEditContainer.tsx @@ -1,6 +1,4 @@ -import LocationDeleteButton from '@/components/admin/locations/LocationDeleteButton'; import React, { useEffect, useState } from 'react'; -import { useHistory } from 'react-router'; import tw from 'twin.macro'; import { useRouteMatch } from 'react-router-dom'; import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy'; @@ -37,7 +35,7 @@ interface Values { } const EditInformationContainer = () => { - const history = useHistory(); + // const history = useHistory(); const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); @@ -100,12 +98,12 @@ const EditInformationContainer = () => {
-
- history.push('/admin/locations')} - /> -
+
+ {/* history.push('/admin/locations')} */} + {/* /> */} +
+ +
+ +
diff --git a/resources/scripts/components/admin/nests/NewNestButton.tsx b/resources/scripts/components/admin/nests/NewNestButton.tsx index b772b695..c7415db1 100644 --- a/resources/scripts/components/admin/nests/NewNestButton.tsx +++ b/resources/scripts/components/admin/nests/NewNestButton.tsx @@ -34,7 +34,7 @@ export default () => { createNest(name, description) .then(nest => { - mutate(data => ({ ...data, items: data.items.concat(nest) }), false); + mutate(data => ({ ...data!, items: data!.items.concat(nest) }), false); setVisible(false); }) .catch(error => { diff --git a/resources/scripts/components/admin/nests/eggs/EggEditContainer.tsx b/resources/scripts/components/admin/nests/eggs/EggEditContainer.tsx deleted file mode 100644 index 54fb6e88..00000000 --- a/resources/scripts/components/admin/nests/eggs/EggEditContainer.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import tw from 'twin.macro'; -import { useRouteMatch } from 'react-router-dom'; -import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy'; -import getEgg, { Egg } from '@/api/admin/eggs/getEgg'; -import AdminContentBlock from '@/components/admin/AdminContentBlock'; -import Spinner from '@/components/elements/Spinner'; -import FlashMessageRender from '@/components/FlashMessageRender'; -import { ApplicationStore } from '@/state'; - -interface ctx { - egg: Egg | undefined; - setEgg: Action; -} - -export const Context = createContextStore({ - egg: undefined, - - setEgg: action((state, payload) => { - state.egg = payload; - }), -}); - -const EggEditContainer = () => { - const match = useRouteMatch<{ nestId?: string, id?: string }>(); - - const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); - const [ loading, setLoading ] = useState(true); - - const egg = Context.useStoreState(state => state.egg); - const setEgg = Context.useStoreActions(actions => actions.setEgg); - - useEffect(() => { - clearFlashes('egg'); - - getEgg(Number(match.params?.id)) - .then(egg => setEgg(egg)) - .catch(error => { - console.error(error); - clearAndAddHttpError({ key: 'egg', error }); - }) - .then(() => setLoading(false)); - }, []); - - if (loading || egg === undefined) { - return ( - - - -
- -
-
- ); - } - - return ( - -
-
-

{egg.name}

- { - (egg.description || '').length < 1 ? -

- No description -

- : -

{egg.description}

- } -
-
- - -
- ); -}; - -export default () => { - return ( - - - - ); -}; diff --git a/resources/scripts/components/admin/nests/eggs/EggInstallContainer.tsx b/resources/scripts/components/admin/nests/eggs/EggInstallContainer.tsx new file mode 100644 index 00000000..0184916c --- /dev/null +++ b/resources/scripts/components/admin/nests/eggs/EggInstallContainer.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import tw from 'twin.macro'; +import AdminBox from '@/components/admin/AdminBox'; +import { Context } from '@/components/admin/nests/eggs/EggRouter'; +import Editor2 from '@/components/elements/Editor2'; +import { shell } from '@codemirror/legacy-modes/mode/shell'; +import Button from '@/components/elements/Button'; +import Input from '@/components/elements/Input'; +import Label from '@/components/elements/Label'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; + +const initialContent = `#!/bin/ash + +curl -s https://cdn.pterodactyl.io/releases/latest.json | jq +`; + +export default () => { + const egg = Context.useStoreState(state => state.egg); + + if (egg === undefined) { + return ( + <> + ); + } + + return ( + +
+ + + + +
+
+
+ + +

The Docker image to use for running this installation script.

+
+ +
+ + +

The command that should be used to run this script inside of the installation container.

+
+
+
+ +
+ +
+
+
+ ); +}; diff --git a/resources/scripts/components/admin/nests/eggs/EggRouter.tsx b/resources/scripts/components/admin/nests/eggs/EggRouter.tsx new file mode 100644 index 00000000..c541b806 --- /dev/null +++ b/resources/scripts/components/admin/nests/eggs/EggRouter.tsx @@ -0,0 +1,117 @@ +import EggInstallContainer from '@/components/admin/nests/eggs/EggInstallContainer'; +import EggVariablesContainer from '@/components/admin/nests/eggs/EggVariablesContainer'; +import React, { useEffect, useState } from 'react'; +import { useLocation } from 'react-router'; +import tw from 'twin.macro'; +import { Route, Switch, useRouteMatch } from 'react-router-dom'; +import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy'; +import getEgg, { Egg } from '@/api/admin/eggs/getEgg'; +import AdminContentBlock from '@/components/admin/AdminContentBlock'; +import Spinner from '@/components/elements/Spinner'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { ApplicationStore } from '@/state'; +import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation'; +import EggSettingsContainer from '@/components/admin/nests/eggs/EggSettingsContainer'; + +interface ctx { + egg: Egg | undefined; + setEgg: Action; +} + +export const Context = createContextStore({ + egg: undefined, + + setEgg: action((state, payload) => { + state.egg = payload; + }), +}); + +const EggRouter = () => { + const location = useLocation(); + const match = useRouteMatch<{ id?: string }>(); + + const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + const [ loading, setLoading ] = useState(true); + + const egg = Context.useStoreState(state => state.egg); + const setEgg = Context.useStoreActions(actions => actions.setEgg); + + useEffect(() => { + clearFlashes('egg'); + + getEgg(Number(match.params?.id)) + .then(egg => setEgg(egg)) + .catch(error => { + console.error(error); + clearAndAddHttpError({ key: 'egg', error }); + }) + .then(() => setLoading(false)); + }, []); + + if (loading || egg === undefined) { + return ( + + + +
+ +
+
+ ); + } + + return ( + +
+
+

{egg.name}

+

{egg.uuid}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default () => { + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/nests/eggs/EggSettingsContainer.tsx b/resources/scripts/components/admin/nests/eggs/EggSettingsContainer.tsx new file mode 100644 index 00000000..e19544f6 --- /dev/null +++ b/resources/scripts/components/admin/nests/eggs/EggSettingsContainer.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import AdminBox from '@/components/admin/AdminBox'; +import { Context } from '@/components/admin/nests/eggs/EggRouter'; + +export default () => { + const egg = Context.useStoreState(state => state.egg); + + if (egg === undefined) { + return ( + <> + ); + } + + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/nests/eggs/EggVariablesContainer.tsx b/resources/scripts/components/admin/nests/eggs/EggVariablesContainer.tsx new file mode 100644 index 00000000..d4df3dcf --- /dev/null +++ b/resources/scripts/components/admin/nests/eggs/EggVariablesContainer.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import AdminBox from '@/components/admin/AdminBox'; +import { Context } from '@/components/admin/nests/eggs/EggRouter'; + +export default () => { + const egg = Context.useStoreState(state => state.egg); + + if (egg === undefined) { + return ( + <> + ); + } + + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/nodes/NodeLimitContainer.tsx b/resources/scripts/components/admin/nodes/NodeLimitContainer.tsx index 69a4d359..8fbd9d42 100644 --- a/resources/scripts/components/admin/nodes/NodeLimitContainer.tsx +++ b/resources/scripts/components/admin/nodes/NodeLimitContainer.tsx @@ -4,19 +4,10 @@ import tw from 'twin.macro'; import Field from '@/components/elements/Field'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import { Form, useFormikContext } from 'formik'; -import { Context } from '@/components/admin/nodes/NodeRouter'; export default () => { const { isSubmitting } = useFormikContext(); - const node = Context.useStoreState(state => state.node); - - if (node === undefined) { - return ( - <> - ); - } - return ( diff --git a/resources/scripts/components/admin/nodes/NodeListenContainer.tsx b/resources/scripts/components/admin/nodes/NodeListenContainer.tsx index cce46f6c..3145f6b1 100644 --- a/resources/scripts/components/admin/nodes/NodeListenContainer.tsx +++ b/resources/scripts/components/admin/nodes/NodeListenContainer.tsx @@ -4,19 +4,10 @@ import tw from 'twin.macro'; import Field from '@/components/elements/Field'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import { Form, useFormikContext } from 'formik'; -import { Context } from '@/components/admin/nodes/NodeRouter'; export default () => { const { isSubmitting } = useFormikContext(); - const node = Context.useStoreState(state => state.node); - - if (node === undefined) { - return ( - <> - ); - } - return ( diff --git a/resources/scripts/components/admin/nodes/NodesContainer.tsx b/resources/scripts/components/admin/nodes/NodesContainer.tsx index ee224d4a..824e8d40 100644 --- a/resources/scripts/components/admin/nodes/NodesContainer.tsx +++ b/resources/scripts/components/admin/nodes/NodesContainer.tsx @@ -1,3 +1,4 @@ +import { Filters } from '@/api/admin/servers/getServers'; import React, { useContext, useEffect, useState } from 'react'; import getNodes, { Context as NodesContext } from '@/api/admin/nodes/getNodes'; import FlashMessageRender from '@/components/FlashMessageRender'; @@ -35,7 +36,7 @@ const RowCheckbox = ({ id }: { id: number}) => { const NodesContainer = () => { const match = useRouteMatch(); - const { page, setPage } = useContext(NodesContext); + const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(NodesContext); const { clearFlashes, clearAndAddHttpError } = useFlash(); const { data: nodes, error, isValidating } = getNodes([ 'location' ]); @@ -57,6 +58,17 @@ const NodesContainer = () => { setSelectedNodes(e.currentTarget.checked ? (nodes?.items?.map(node => node.id) || []) : []); }; + const onSearch = (query: string): Promise => { + return new Promise((resolve) => { + if (query.length < 2) { + setFilters(null); + } else { + setFilters({ name: query }); + } + return resolve(); + }); + }; + useEffect(() => { setSelectedNodes([]); }, [ page ]); @@ -90,17 +102,18 @@ const NodesContainer = () => {
- - - - - - + setSort('id')}/> + setSort('name')}/> + setSort('location_id')}/> + setSort('fqdn')}/> + setSort('memory')}/> + setSort('disk')}/> @@ -181,9 +194,21 @@ const NodesContainer = () => { export default () => { const [ page, setPage ] = useState(1); + const [ filters, setFilters ] = useState(null); + const [ sort, setSortState ] = useState(null); + const [ sortDirection, setSortDirection ] = useState(false); + + const setSort = (newSort: string | null) => { + if (sort === newSort) { + setSortDirection(!sortDirection); + } else { + setSortState(newSort); + setSortDirection(false); + } + }; return ( - + ); diff --git a/resources/scripts/components/admin/servers/OwnerSelect.tsx b/resources/scripts/components/admin/servers/OwnerSelect.tsx new file mode 100644 index 00000000..603a5240 --- /dev/null +++ b/resources/scripts/components/admin/servers/OwnerSelect.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { useFormikContext } from 'formik'; +import { User } from '@/api/admin/users/getUsers'; +import searchUsers from '@/api/admin/users/searchUsers'; +import SearchableSelect, { Option } from '@/components/elements/SearchableSelect'; + +export default ({ selected }: { selected: User | null }) => { + const context = useFormikContext(); + + const [ user, setUser ] = useState(selected); + const [ users, setUsers ] = useState(null); + + const onSearch = (query: string): Promise => { + return new Promise((resolve, reject) => { + searchUsers({ username: query, email: query }).then((users) => { + setUsers(users); + return resolve(); + }).catch(reject); + }); + }; + + const onSelect = (user: User | null) => { + setUser(user); + context.setFieldValue('ownerId', user?.id || null); + }; + + const getSelectedText = (user: User | null): string => { + return user?.username || ''; + }; + + return ( + + {users?.map(d => ( + + ))} + + ); +}; diff --git a/resources/scripts/components/admin/servers/ServerManageContainer.tsx b/resources/scripts/components/admin/servers/ServerManageContainer.tsx new file mode 100644 index 00000000..56c82b28 --- /dev/null +++ b/resources/scripts/components/admin/servers/ServerManageContainer.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import AdminBox from '@/components/admin/AdminBox'; +import tw from 'twin.macro'; +import { Context } from '@/components/admin/servers/ServerRouter'; +import Button from '@/components/elements/Button'; + +const ServerManageContainer = () => { + const server = Context.useStoreState(state => state.server); + + if (server === undefined) { + return ( + <> + ); + } + + return ( +
+
+ +
+
+ + + +
+

+ Danger! This could overwrite server data. +

+
+ +

+ This will reinstall the server with the assigned service scripts. +

+
+
+
+ + +

+ If you need to change the install status from uninstalled to installed, or vice versa, you may do so with the button below. +

+
+
+
+ + +

+ This will suspend the server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the server through the panel or API. +

+
+
+
+ ); +}; + +export default () => { + const server = Context.useStoreState(state => state.server); + + if (server === undefined) { + return ( + <> + ); + } + + return ( + + ); +}; diff --git a/resources/scripts/components/admin/servers/ServerRouter.tsx b/resources/scripts/components/admin/servers/ServerRouter.tsx index e69de29b..7051fa1f 100644 --- a/resources/scripts/components/admin/servers/ServerRouter.tsx +++ b/resources/scripts/components/admin/servers/ServerRouter.tsx @@ -0,0 +1,130 @@ +import ServerManageContainer from '@/components/admin/servers/ServerManageContainer'; +import ServerStartupContainer from '@/components/admin/servers/ServerStartupContainer'; +import React, { useEffect, useState } from 'react'; +import { useLocation } from 'react-router'; +import tw from 'twin.macro'; +import { Route, Switch, useRouteMatch } from 'react-router-dom'; +import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy'; +import { Server } from '@/api/admin/servers/getServers'; +import getServer from '@/api/admin/servers/getServer'; +import AdminContentBlock from '@/components/admin/AdminContentBlock'; +import Spinner from '@/components/elements/Spinner'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { ApplicationStore } from '@/state'; +import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation'; +import ServerSettingsContainer from '@/components/admin/servers/ServerSettingsContainer'; + +interface ctx { + server: Server | undefined; + setServer: Action; +} + +export const Context = createContextStore({ + server: undefined, + + setServer: action((state, payload) => { + state.server = payload; + }), +}); + +const ServerRouter = () => { + const location = useLocation(); + const match = useRouteMatch<{ id?: string }>(); + + const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + const [ loading, setLoading ] = useState(true); + + const server = Context.useStoreState(state => state.server); + const setServer = Context.useStoreActions(actions => actions.setServer); + + useEffect(() => { + clearFlashes('server'); + + getServer(Number(match.params?.id), [ 'egg' ]) + .then(server => setServer(server)) + .catch(error => { + console.error(error); + clearAndAddHttpError({ key: 'server', error }); + }) + .then(() => setLoading(false)); + }, []); + + if (loading || server === undefined) { + return ( + + + +
+ +
+
+ ); + } + + return ( + +
+
+

{server.name}

+

{server.uuid}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default () => { + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx b/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx index e69de29b..b79c8b00 100644 --- a/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx +++ b/resources/scripts/components/admin/servers/ServerSettingsContainer.tsx @@ -0,0 +1,331 @@ +import React from 'react'; +import AdminBox from '@/components/admin/AdminBox'; +import tw from 'twin.macro'; +import { object } from 'yup'; +import updateServer from '@/api/admin/servers/updateServer'; +import Field from '@/components/elements/Field'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; +import { Context } from '@/components/admin/servers/ServerRouter'; +import { ApplicationStore } from '@/state'; +import { Actions, useStoreActions } from 'easy-peasy'; +import OwnerSelect from '@/components/admin/servers/OwnerSelect'; +import Button from '@/components/elements/Button'; +import FormikSwitch from '@/components/elements/FormikSwitch'; + +interface Values { + id: number; + externalId: string; + uuid: string; + identifier: string; + name: string; + description: string; + + memory: number; + swap: number; + disk: number; + io: number; + cpu: number; + threads: string; + + databases: number; + allocations: number; + backups: number; + + ownerId: number; + nodeId: number; + allocationId: number; + nestId: number; + eggId: number; +} + +const ServerFeatureContainer = () => { + const { isSubmitting } = useFormikContext(); + + const server = Context.useStoreState(state => state.server); + + if (server === undefined) { + return ( + <> + ); + } + + return ( + + + +
+
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ + +
+ ); +}; + +const ServerResourceContainer = () => { + const { isSubmitting } = useFormikContext(); + + const server = Context.useStoreState(state => state.server); + + if (server === undefined) { + return ( + <> + ); + } + + return ( + + + +
+
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ + +
+ ); +}; + +const ServerSettingsContainer = () => { + const { isSubmitting } = useFormikContext(); + + const server = Context.useStoreState(state => state.server); + + if (server === undefined) { + return ( + <> + ); + } + + return ( + + + +
+
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+ ); +}; + +export default () => { + const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + + const server = Context.useStoreState(state => state.server); + const setServer = Context.useStoreActions(actions => actions.setServer); + + if (server === undefined) { + return ( + <> + ); + } + + const submit = (values: Values, { setSubmitting }: FormikHelpers) => { + clearFlashes('server'); + + updateServer(server.id, values) + .then(() => setServer({ ...server, ...values })) + .catch(error => { + console.error(error); + clearAndAddHttpError({ key: 'server', error }); + }) + .then(() => setSubmitting(false)); + }; + + return ( + + { + ({ isSubmitting, isValid }) => ( +
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+
+ ) + } +
+ ); +}; diff --git a/resources/scripts/components/admin/servers/ServerStartupContainer.tsx b/resources/scripts/components/admin/servers/ServerStartupContainer.tsx new file mode 100644 index 00000000..20d9ad65 --- /dev/null +++ b/resources/scripts/components/admin/servers/ServerStartupContainer.tsx @@ -0,0 +1,203 @@ +import React from 'react'; +import Button from '@/components/elements/Button'; +import FormikSwitch from '@/components/elements/FormikSwitch'; +import Input from '@/components/elements/Input'; +import AdminBox from '@/components/admin/AdminBox'; +import tw from 'twin.macro'; +import { object } from 'yup'; +import updateServer from '@/api/admin/servers/updateServer'; +import Field from '@/components/elements/Field'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; +import { Context } from '@/components/admin/servers/ServerRouter'; +import { ApplicationStore } from '@/state'; +import { Actions, useStoreActions } from 'easy-peasy'; +import Label from '@/components/elements/Label'; +// import { ServerEggVariable } from '@/api/server/types'; + +/* interface Props { + variable: ServerEggVariable; +} */ + +interface Values { + startupCommand: string; + nestId: number; + eggId: number; +} + +/* const VariableBox = ({ variable }: Props) => { + const { isSubmitting } = useFormikContext(); + + const server = Context.useStoreState(state => state.server); + + if (server === undefined) { + return ( + <> + ); + } + + return ( + + + +
+
+ +
+ +
+ ); +}; */ + +const ServerServiceContainer = () => { + const { isSubmitting } = useFormikContext(); + + const server = Context.useStoreState(state => state.server); + + if (server === undefined) { + return ( + <> + ); + } + + return ( + + + +
+
+
+
+

+ This is a destructive operation in many cases. This server will be stopped immediately in order for this action to proceed. +

+
+
+

+ Changing any of the below values will result in the server processing a re-install command. The server will be stopped and will then proceed. If you would like the service scripts to not run, ensure the box is checked at the bottom. +

+
+
+
+ Nest/Egg Selector HERE +
+
+ +
+
+ +
+ ); +}; + +const ServerStartupContainer = () => { + const { isSubmitting } = useFormikContext(); + + const server = Context.useStoreState(state => state.server); + + if (server === undefined) { + return ( + <> + ); + } + + return ( + + + +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ ); +}; + +export default () => { + const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + + const server = Context.useStoreState(state => state.server); + const setServer = Context.useStoreActions(actions => actions.setServer); + + if (server === undefined) { + return ( + <> + ); + } + + const submit = (values: Values, { setSubmitting }: FormikHelpers) => { + clearFlashes('server'); + + updateServer(server.id, values) + .then(() => setServer({ ...server, ...values })) + .catch(error => { + console.error(error); + clearAndAddHttpError({ key: 'server', error }); + }) + .then(() => setSubmitting(false)); + }; + + return ( + + { + ({ isSubmitting, isValid }) => ( +
+
+ +
+
+ +
+
+ Server Startup variables go here +
+
+
+ +
+
+
+ ) + } +
+ ); +}; diff --git a/resources/scripts/components/admin/servers/ServersContainer.tsx b/resources/scripts/components/admin/servers/ServersContainer.tsx index 3b94d67f..cf2127c9 100644 --- a/resources/scripts/components/admin/servers/ServersContainer.tsx +++ b/resources/scripts/components/admin/servers/ServersContainer.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useState } from 'react'; -import getServers, { Context as ServersContext } from '@/api/admin/servers/getServers'; +import getServers, { Context as ServersContext, Filters } from '@/api/admin/servers/getServers'; import AdminCheckbox from '@/components/admin/AdminCheckbox'; -import AdminTable, { ContentWrapper, Loading, NoItems, Pagination, TableBody, TableHead, TableHeader } from '@/components/admin/AdminTable'; +import AdminTable, { ContentWrapper, Loading, Pagination, TableBody, TableHead, TableHeader } from '@/components/admin/AdminTable'; import Button from '@/components/elements/Button'; import CopyOnClick from '@/components/elements/CopyOnClick'; import FlashMessageRender from '@/components/FlashMessageRender'; @@ -31,21 +31,12 @@ const RowCheckbox = ({ id }: { id: number }) => { ); }; -const UsersContainer = () => { +const ServersContainer = () => { const match = useRouteMatch(); - const { page, setPage } = useContext(ServersContext); + const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(ServersContext); const { clearFlashes, clearAndAddHttpError } = useFlash(); - const { data: servers, error, isValidating } = getServers([ 'node', 'user' ]); - - useEffect(() => { - if (!error) { - clearFlashes('servers'); - return; - } - - clearAndAddHttpError({ key: 'servers', error }); - }, [ error ]); + const { data: servers, error } = getServers([ 'node', 'user' ]); const length = servers?.items?.length || 0; @@ -56,10 +47,30 @@ const UsersContainer = () => { setSelectedServers(e.currentTarget.checked ? (servers?.items?.map(server => server.id) || []) : []); }; + const onSearch = (query: string): Promise => { + return new Promise((resolve) => { + if (query.length < 2) { + setFilters(null); + } else { + setFilters({ name: query }); + } + return resolve(); + }); + }; + useEffect(() => { setSelectedServers([]); }, [ page ]); + useEffect(() => { + if (!error) { + clearFlashes('servers'); + return; + } + + clearAndAddHttpError({ key: 'servers', error }); + }, [ error ]); + return (
@@ -80,102 +91,103 @@ const UsersContainer = () => { - { servers === undefined || (error && isValidating) ? - - : - length < 1 ? - + + {servers === undefined ? + : - - -
-
- - - - - - - + // length < 1 ? + // + // : + +
+
+ + setSort('uuidShort')}/> + setSort('name')}/> + setSort('owner_id')}/> + setSort('node_id')}/> + setSort('status')}/> + - - { - servers.items.map(server => ( - - + + { + servers?.items.map(server => ( + + - + - + - {/* TODO: Have permission check for displaying user information. */} - +
+ {server.relations.user?.email} +
+ + - {/* TODO: Have permission check for displaying node information. */} - +
+ {server.relations.node?.fqdn} +
+ + - - - )) - } - -
- -
+ + - - {server.identifier} - - + + {server.identifier} + + - - {server.name} - - + + {server.name} + + - -
- {server.relations.user?.firstName} {server.relations.user?.lastName} -
+ {/* TODO: Have permission check for displaying user information. */} +
+ +
+ {server.relations.user?.firstName} {server.relations.user?.lastName} +
-
- {server.relations.user?.email} -
-
-
- -
- {server.relations.node?.name} -
+ {/* TODO: Have permission check for displaying node information. */} +
+ +
+ {server.relations.node?.name} +
-
- {server.relations.node?.fqdn} -
-
-
- { server.isInstalling ? + + {server.status === 'installing' ? + + Installing + + : + server.status === 'transferring' ? - Installing + Transferring - : - server.isTransferring ? - - Transferring + : server.status === 'suspended' ? + + Suspended - : server.isSuspended ? - - Suspended - - : - - Active - - } -
-
-
-
- } + : + + Active + + } + + + )) + } + + + + + } + ); @@ -183,10 +195,22 @@ const UsersContainer = () => { export default () => { const [ page, setPage ] = useState(1); + const [ filters, setFilters ] = useState(null); + const [ sort, setSortState ] = useState(null); + const [ sortDirection, setSortDirection ] = useState(false); + + const setSort = (newSort: string | null) => { + if (sort === newSort) { + setSortDirection(!sortDirection); + } else { + setSortState(newSort); + setSortDirection(false); + } + }; return ( - - + + ); }; diff --git a/resources/scripts/components/elements/Editor2.tsx b/resources/scripts/components/elements/Editor2.tsx new file mode 100644 index 00000000..6dbabb0d --- /dev/null +++ b/resources/scripts/components/elements/Editor2.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useState } from 'react'; +import styled from 'styled-components/macro'; +import tw, { TwStyle } from 'twin.macro'; +import { autocompletion, completionKeymap } from '@codemirror/autocomplete'; +import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets'; +import { defaultKeymap, defaultTabBinding } from '@codemirror/commands'; +import { commentKeymap } from '@codemirror/comment'; +import { foldGutter, foldKeymap } from '@codemirror/fold'; +import { lineNumbers, highlightActiveLineGutter } from '@codemirror/gutter'; +import { defaultHighlightStyle } from '@codemirror/highlight'; +import { history, historyKeymap } from '@codemirror/history'; +import { indentOnInput, LezerLanguage } from '@codemirror/language'; +import { lintKeymap } from '@codemirror/lint'; +import { bracketMatching } from '@codemirror/matchbrackets'; +import { rectangularSelection } from '@codemirror/rectangular-selection'; +import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; +import { Extension, EditorState } from '@codemirror/state'; +import { StreamLanguage, StreamParser } from '@codemirror/stream-parser'; +import { keymap, highlightSpecialChars, drawSelection, highlightActiveLine, EditorView } from '@codemirror/view'; + +import { ayuMirage } from '@/components/elements/EditorTheme'; + +const extensions: Extension = [ + ayuMirage, + + lineNumbers(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + foldGutter(), + drawSelection(), + EditorState.allowMultipleSelections.of(true), + indentOnInput(), + defaultHighlightStyle.fallback, + bracketMatching(), + closeBrackets(), + autocompletion(), + rectangularSelection(), + highlightActiveLine(), + highlightSelectionMatches(), + keymap.of([ + ...closeBracketsKeymap, + ...defaultKeymap, + ...searchKeymap, + ...historyKeymap, + ...foldKeymap, + ...commentKeymap, + ...completionKeymap, + ...lintKeymap, + defaultTabBinding, + ]), + + EditorState.tabSize.of(4), +]; + +const EditorContainer = styled.div<{ overrides?: TwStyle }>` + min-height: 12rem; + ${tw`relative`}; + + & > div { + ${props => props.overrides}; + + &.cm-focused { + outline: none; + } + } +`; + +export interface Props { + className?: string; + overrides?: TwStyle; + mode: LezerLanguage | StreamParser; + initialContent?: string; +} + +export default ({ className, overrides, mode, initialContent }: Props) => { + const [ state ] = useState(EditorState.create({ + doc: initialContent, + extensions: [ ...extensions, (mode instanceof LezerLanguage) ? mode : StreamLanguage.define(mode) ], + })); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [ view, setView ] = useState(); + + const ref = useCallback((node) => { + if (!node) { + return; + } + + const view = new EditorView({ + state: state, + parent: node, + }); + setView(view); + }, []); + + return ( + + ); +}; diff --git a/resources/scripts/components/elements/EditorTheme.ts b/resources/scripts/components/elements/EditorTheme.ts new file mode 100644 index 00000000..fbbd9ece --- /dev/null +++ b/resources/scripts/components/elements/EditorTheme.ts @@ -0,0 +1,144 @@ +import { EditorView } from '@codemirror/view'; +import { Extension } from '@codemirror/state'; +import { HighlightStyle, tags as t } from '@codemirror/highlight'; + +const highlightBackground = 'transparent'; +const background = '#1F2430'; +const selection = '#34455A'; +const cursor = '#FFCC66'; + +export const ayuMirageTheme = 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', + }, +]); + +export const ayuMirage: Extension = [ ayuMirageTheme, ayuMirageHighlightStyle ]; diff --git a/resources/scripts/components/elements/Input.tsx b/resources/scripts/components/elements/Input.tsx index d26ea6d7..d326229f 100644 --- a/resources/scripts/components/elements/Input.tsx +++ b/resources/scripts/components/elements/Input.tsx @@ -43,7 +43,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-400`}; } &:required, &:invalid { diff --git a/resources/scripts/components/server/backups/BackupContextMenu.tsx b/resources/scripts/components/server/backups/BackupContextMenu.tsx index 146ab50e..48a3a29c 100644 --- a/resources/scripts/components/server/backups/BackupContextMenu.tsx +++ b/resources/scripts/components/server/backups/BackupContextMenu.tsx @@ -1,12 +1,5 @@ import React, { useState } from 'react'; -import { - faBoxOpen, - faCloudDownloadAlt, - faEllipsisH, - faLock, - faTrashAlt, - faUnlock, -} from '@fortawesome/free-solid-svg-icons'; +import { faBoxOpen, faCloudDownloadAlt, faEllipsisH, faLock, faTrashAlt, faUnlock } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import DropdownMenu, { DropdownButtonRow } from '@/components/elements/DropdownMenu'; import getBackupDownloadUrl from '@/api/server/backups/getBackupDownloadUrl'; @@ -56,8 +49,8 @@ export default ({ backup }: Props) => { clearFlashes('backups'); deleteBackup(uuid, backup.uuid) .then(() => mutate(data => ({ - ...data, - items: data.items.filter(b => b.uuid !== backup.uuid), + ...data!, + items: data!.items.filter(b => b.uuid !== backup.uuid), }), false)) .catch(error => { console.error(error); @@ -90,8 +83,8 @@ 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 : { + ...data!, + items: data!.items.map(b => b.uuid !== backup.uuid ? b : { ...b, isLocked: !b.isLocked, }), @@ -124,13 +117,13 @@ export default ({ backup }: Props) => { not be able to control the server power state, access the file manager, or create additional backups until it has completed.

-

+

Are you sure you want to continue?

-

+