diff --git a/app/Http/Controllers/Api/Application/Locations/LocationController.php b/app/Http/Controllers/Api/Application/Locations/LocationController.php index f737bcc47..a3a4af6e7 100644 --- a/app/Http/Controllers/Api/Application/Locations/LocationController.php +++ b/app/Http/Controllers/Api/Application/Locations/LocationController.php @@ -86,11 +86,6 @@ class LocationController extends ApplicationApiController return $this->fractal->item($location) ->transformWith(LocationTransformer::class) - ->addMeta([ - 'resource' => route('api.application.locations.view', [ - 'location' => $location->id, - ]), - ]) ->respond(201); } diff --git a/app/Http/Controllers/Api/Application/Nodes/NodeController.php b/app/Http/Controllers/Api/Application/Nodes/NodeController.php index ef06336f7..c9cfbb857 100644 --- a/app/Http/Controllers/Api/Application/Nodes/NodeController.php +++ b/app/Http/Controllers/Api/Application/Nodes/NodeController.php @@ -90,11 +90,6 @@ class NodeController extends ApplicationApiController return $this->fractal->item($node) ->transformWith(NodeTransformer::class) - ->addMeta([ - 'resource' => route('api.application.nodes.view', [ - 'node' => $node->id, - ]), - ]) ->respond(201); } diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index 5089e1e0c..b6b71659d 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -87,12 +87,6 @@ class DatabaseController extends ApplicationApiController return $this->fractal->item($database) ->transformWith(ServerDatabaseTransformer::class) - ->addMeta([ - 'resource' => route('api.application.servers.databases.view', [ - 'server' => $server->id, - 'database' => $database->id, - ]), - ]) ->respond(Response::HTTP_CREATED); } diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index 592192183..8cb40cc88 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -115,11 +115,6 @@ class UserController extends ApplicationApiController return $this->fractal->item($user) ->transformWith(UserTransformer::class) - ->addMeta([ - 'resource' => route('api.application.users.view', [ - 'user' => $user->id, - ]), - ]) ->respond(201); } diff --git a/resources/scripts/api/admin/nodes/createNode.ts b/resources/scripts/api/admin/nodes/createNode.ts index 4d2397e98..8143a92c3 100644 --- a/resources/scripts/api/admin/nodes/createNode.ts +++ b/resources/scripts/api/admin/nodes/createNode.ts @@ -1,11 +1,41 @@ import http from '@/api/http'; import { Node, rawDataToNode } from '@/api/admin/nodes/getNodes'; -export default (name: string, description: string | null, include: string[] = []): Promise => { +export interface Values { + name: string; + locationId: number; + databaseHostId: number | null; + fqdn: string; + scheme: string; + behindProxy: boolean; + public: boolean; + daemonBase: string; + + memory: number; + memoryOverallocate: number; + disk: number; + diskOverallocate: number; + + listenPortHTTP: number; + publicPortHTTP: number; + listenPortSFTP: number; + publicPortSFTP: number; +} + +export default (values: Values, include: string[] = []): Promise => { + const data = {}; + + Object.keys(values).forEach((key) => { + const key2 = key + .replace('HTTP', 'Http') + .replace('SFTP', 'Sftp') + .replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); + // @ts-ignore + data[key2] = values[key]; + }); + return new Promise((resolve, reject) => { - http.post('/api/application/nodes', { - name, description, - }, { params: { include: include.join(',') } }) + http.post('/api/application/nodes', data, { params: { include: include.join(',') } }) .then(({ data }) => resolve(rawDataToNode(data))) .catch(reject); }); diff --git a/resources/scripts/api/admin/nodes/updateNode.ts b/resources/scripts/api/admin/nodes/updateNode.ts index bbda4655c..623f66931 100644 --- a/resources/scripts/api/admin/nodes/updateNode.ts +++ b/resources/scripts/api/admin/nodes/updateNode.ts @@ -14,9 +14,7 @@ export default (id: number, node: Partial, include: string[] = []): Promis }); return new Promise((resolve, reject) => { - http.patch(`/api/application/nodes/${id}`, { - ...data, - }, { params: { include: include.join(',') } }) + http.patch(`/api/application/nodes/${id}`, data, { params: { include: include.join(',') } }) .then(({ data }) => resolve(rawDataToNode(data))) .catch(reject); }); diff --git a/resources/scripts/components/admin/nodes/DatabaseSelect.tsx b/resources/scripts/components/admin/nodes/DatabaseSelect.tsx index b6381fbff..03f164092 100644 --- a/resources/scripts/components/admin/nodes/DatabaseSelect.tsx +++ b/resources/scripts/components/admin/nodes/DatabaseSelect.tsx @@ -34,8 +34,8 @@ export default ({ selected }: { selected: Database | null }) => { , 'public'> & { behindProxy: string; public: string }; + +const initialValues: Values2 = { + name: '', + locationId: 0, + databaseHostId: null, + fqdn: '', + scheme: 'https', + behindProxy: 'false', + public: 'true', + daemonBase: '/var/lib/pterodactyl/volumes', + + listenPortHTTP: 8080, + publicPortHTTP: 8080, + listenPortSFTP: 2022, + publicPortSFTP: 2022, + + memory: 0, + memoryOverallocate: 0, + disk: 0, + diskOverallocate: 0, +}; export default () => { + const history = useHistory(); + + const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + + const submit = (values2: Values2, { setSubmitting }: FormikHelpers) => { + clearFlashes('node:create'); + + const values: Values = { ...values2, behindProxy: values2.behindProxy === 'true', public: values2.public === 'true' }; + + createNode(values) + .then(node => history.push(`/admin/nodes/${node.id}`)) + .catch(error => { + console.error(error); + clearAndAddHttpError({ key: 'node:create', error }); + }) + .then(() => setSubmitting(false)); + }; + return (
@@ -11,6 +63,56 @@ export default () => {

Add a new node to the panel.

+ + + + + { + ({ isSubmitting, isValid }) => ( +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+
+
+ ) + } +
); }; diff --git a/resources/scripts/components/admin/nodes/NodeDeleteButton.tsx b/resources/scripts/components/admin/nodes/NodeDeleteButton.tsx new file mode 100644 index 000000000..e7920b269 --- /dev/null +++ b/resources/scripts/components/admin/nodes/NodeDeleteButton.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import tw from 'twin.macro'; +import Button from '@/components/elements/Button'; +import ConfirmationModal from '@/components/elements/ConfirmationModal'; +import deleteNode from '@/api/admin/nodes/deleteNode'; + +interface Props { + nodeId: number; + onDeleted: () => void; +} + +export default ({ nodeId, onDeleted }: Props) => { + const [ visible, setVisible ] = useState(false); + const [ loading, setLoading ] = useState(false); + + const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + + const onDelete = () => { + setLoading(true); + clearFlashes('node'); + + deleteNode(nodeId) + .then(() => { + setLoading(false); + onDeleted(); + }) + .catch(error => { + console.error(error); + clearAndAddHttpError({ key: 'node', error }); + + setLoading(false); + setVisible(false); + }); + }; + + return ( + <> + setVisible(false)} + > + Are you sure you want to delete this node? + + + + + ); +}; diff --git a/resources/scripts/components/admin/nodes/NodeEditContainer.tsx b/resources/scripts/components/admin/nodes/NodeEditContainer.tsx new file mode 100644 index 000000000..f37ba26ea --- /dev/null +++ b/resources/scripts/components/admin/nodes/NodeEditContainer.tsx @@ -0,0 +1,135 @@ +import updateNode from '@/api/admin/nodes/updateNode'; +import NodeDeleteButton from '@/components/admin/nodes/NodeDeleteButton'; +import NodeLimitContainer from '@/components/admin/nodes/NodeLimitContainer'; +import NodeListenContainer from '@/components/admin/nodes/NodeListenContainer'; +import { Context } from '@/components/admin/nodes/NodeRouter'; +import NodeSettingsContainer from '@/components/admin/nodes/NodeSettingsContainer'; +import Button from '@/components/elements/Button'; +import { ApplicationStore } from '@/state'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { Form, Formik, FormikHelpers } from 'formik'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import tw from 'twin.macro'; +import { number, object, string } from 'yup'; + +interface Values { + name: string; + locationId: number; + databaseHostId: number | null; + fqdn: string; + scheme: string; + behindProxy: boolean; + public: boolean; + daemonBase: string; // This value cannot be updated once a node has been created. + + memory: number; + memoryOverallocate: number; + disk: number; + diskOverallocate: number; + + listenPortHTTP: number; + publicPortHTTP: number; + listenPortSFTP: number; + publicPortSFTP: number; +} + +export default () => { + const history = useHistory(); + + const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + + const node = Context.useStoreState(state => state.node); + const setNode = Context.useStoreActions(actions => actions.setNode); + + if (node === undefined) { + return ( + <> + ); + } + + const submit = (values: Values, { setSubmitting }: FormikHelpers) => { + clearFlashes('node'); + + updateNode(node.id, values) + .then(() => setNode({ ...node, ...values })) + .catch(error => { + console.error(error); + clearAndAddHttpError({ key: 'node', error }); + }) + .then(() => setSubmitting(false)); + }; + + return ( + + { + ({ isSubmitting, isValid }) => ( +
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ history.push('/admin/nodes')} + /> + +
+
+
+
+
+ ) + } +
+ ); +}; diff --git a/resources/scripts/components/admin/nodes/NodeLimitContainer.tsx b/resources/scripts/components/admin/nodes/NodeLimitContainer.tsx index 93fc0c3bf..0fe4f620b 100644 --- a/resources/scripts/components/admin/nodes/NodeLimitContainer.tsx +++ b/resources/scripts/components/admin/nodes/NodeLimitContainer.tsx @@ -1,3 +1,4 @@ +import { faMicrochip } from '@fortawesome/free-solid-svg-icons'; import React from 'react'; import AdminBox from '@/components/admin/AdminBox'; import tw from 'twin.macro'; @@ -9,7 +10,7 @@ export default () => { const { isSubmitting } = useFormikContext(); return ( - +
diff --git a/resources/scripts/components/admin/nodes/NodeListenContainer.tsx b/resources/scripts/components/admin/nodes/NodeListenContainer.tsx index 69870d962..2a54c5fa7 100644 --- a/resources/scripts/components/admin/nodes/NodeListenContainer.tsx +++ b/resources/scripts/components/admin/nodes/NodeListenContainer.tsx @@ -1,3 +1,4 @@ +import { faNetworkWired } from '@fortawesome/free-solid-svg-icons'; import React from 'react'; import AdminBox from '@/components/admin/AdminBox'; import tw from 'twin.macro'; @@ -9,7 +10,7 @@ export default () => { const { isSubmitting } = useFormikContext(); return ( - +
diff --git a/resources/scripts/components/admin/nodes/NodeRouter.tsx b/resources/scripts/components/admin/nodes/NodeRouter.tsx index a8b6b4718..a339b4e0a 100644 --- a/resources/scripts/components/admin/nodes/NodeRouter.tsx +++ b/resources/scripts/components/admin/nodes/NodeRouter.tsx @@ -1,3 +1,4 @@ +import NodeEditContainer from '@/components/admin/nodes/NodeEditContainer'; import React, { useEffect, useState } from 'react'; import { useLocation } from 'react-router'; import tw from 'twin.macro'; @@ -11,7 +12,6 @@ import FlashMessageRender from '@/components/FlashMessageRender'; import { ApplicationStore } from '@/state'; import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation'; import NodeAboutContainer from '@/components/admin/nodes/NodeAboutContainer'; -import NodeSettingsContainer from '@/components/admin/nodes/NodeSettingsContainer'; import NodeConfigurationContainer from '@/components/admin/nodes/NodeConfigurationContainer'; import NodeAllocationContainer from '@/components/admin/nodes/NodeAllocationContainer'; import NodeServers from '@/components/admin/nodes/NodeServers'; @@ -112,7 +112,7 @@ const NodeRouter = () => { - + diff --git a/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx b/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx index f94b0cf73..b16d52041 100644 --- a/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx +++ b/resources/scripts/components/admin/nodes/NodeSettingsContainer.tsx @@ -1,54 +1,20 @@ -import Button from '@/components/elements/Button'; +import { Node } from '@/api/admin/nodes/getNodes'; +import { faDatabase } from '@fortawesome/free-solid-svg-icons'; import React from 'react'; import AdminBox from '@/components/admin/AdminBox'; import tw from 'twin.macro'; -import { number, object, string } from 'yup'; -import updateNode from '@/api/admin/nodes/updateNode'; import Field from '@/components/elements/Field'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import { Field as FormikField, Form, Formik, FormikHelpers, useFormikContext } from 'formik'; -import { Context } from '@/components/admin/nodes/NodeRouter'; -import { ApplicationStore } from '@/state'; -import { Actions, useStoreActions } from 'easy-peasy'; +import { Field as FormikField, useFormikContext } from 'formik'; import LocationSelect from '@/components/admin/nodes/LocationSelect'; import DatabaseSelect from '@/components/admin/nodes/DatabaseSelect'; import Label from '@/components/elements/Label'; -import NodeLimitContainer from '@/components/admin/nodes/NodeLimitContainer'; -import NodeListenContainer from '@/components/admin/nodes/NodeListenContainer'; -interface Values { - name: string; - locationId: number; - databaseHostId: number | null; - fqdn: string; - scheme: string; - behindProxy: boolean; - public: boolean; - - memory: number; - memoryOverallocate: number; - disk: number; - diskOverallocate: number; - - listenPortHTTP: number; - publicPortHTTP: number; - listenPortSFTP: number; - publicPortSFTP: number; -} - -const NodeSettingsContainer = () => { +export default function NodeSettingsContainer ({ node }: { node?: Node }) { const { isSubmitting } = useFormikContext(); - const node = Context.useStoreState(state => state.node); - - if (node === undefined) { - return ( - <> - ); - } - return ( - +
@@ -77,6 +43,16 @@ const NodeSettingsContainer = () => { />
+
+ +
+
@@ -109,7 +85,7 @@ const NodeSettingsContainer = () => { No @@ -118,7 +94,7 @@ const NodeSettingsContainer = () => { Yes @@ -133,7 +109,7 @@ const NodeSettingsContainer = () => { Disabled @@ -142,7 +118,7 @@ const NodeSettingsContainer = () => { Enabled @@ -150,98 +126,4 @@ const NodeSettingsContainer = () => {
); -}; - -export default () => { - const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); - - const node = Context.useStoreState(state => state.node); - const setNode = Context.useStoreActions(actions => actions.setNode); - - if (node === undefined) { - return ( - <> - ); - } - - const submit = (values: Values, { setSubmitting }: FormikHelpers) => { - console.log('submit!'); - clearFlashes('node'); - - updateNode(node.id, values) - .then(() => setNode({ ...node, ...values })) - .catch(error => { - console.error(error); - clearAndAddHttpError({ key: 'node', error }); - }) - .then(() => setSubmitting(false)); - }; - - return ( - - { - ({ isSubmitting, isValid }) => ( -
-
-
- -
- -
-
- -
- -
- -
- -
-
- -
-
-
-
-
- ) - } -
- ); -}; +} diff --git a/resources/scripts/components/admin/nodes/allocations/CreateAllocationForm.tsx b/resources/scripts/components/admin/nodes/allocations/CreateAllocationForm.tsx index 55cf714c2..dc95d73f1 100644 --- a/resources/scripts/components/admin/nodes/allocations/CreateAllocationForm.tsx +++ b/resources/scripts/components/admin/nodes/allocations/CreateAllocationForm.tsx @@ -35,7 +35,7 @@ function CreateAllocationForm ({ nodeId }: { nodeId: string | number }) { }, [ nodeId ]); const isValidIP = (inputValue: string): boolean => { - // TODO: Better way of checking for a valid ip (and CIDR). + // TODO: Better way of checking for a valid ip (and CIDR) return inputValue.match(/^([0-9a-f.:/]+)$/) !== null; }; @@ -44,11 +44,11 @@ function CreateAllocationForm ({ nodeId }: { nodeId: string | number }) { return inputValue.match(/^([0-9-]+)$/) !== null; }; - const submit = (values: Values, { setSubmitting }: FormikHelpers) => { + const submit = ({ ips, ports, alias }: Values, { setSubmitting }: FormikHelpers) => { setSubmitting(false); - values.ips.forEach(async (ip) => { - const allocations = await createAllocation(nodeId, { ip, ports: values.ports, alias: values.alias }, [ 'server' ]); + ips.forEach(async (ip) => { + const allocations = await createAllocation(nodeId, { ip, ports, alias }, [ 'server' ]); await mutate(data => ({ ...data!, items: { ...data!.items!, ...allocations } })); }); };