diff --git a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php index 3e42ab62a..52d798a34 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -18,15 +18,9 @@ class StoreServerRequest extends ApplicationApiRequest 'external_id' => $rules['external_id'], 'name' => $rules['name'], 'description' => array_merge(['nullable'], $rules['description']), - 'user' => $rules['owner_id'], - 'egg' => $rules['egg_id'], - 'docker_image' => $rules['image'], - 'startup' => $rules['startup'], - 'environment' => 'present|array', - 'skip_scripts' => 'sometimes|boolean', - 'oom_disabled' => 'sometimes|boolean', + 'owner_id' => $rules['owner_id'], + 'node_id' => $rules['node_id'], - // Resource limitations 'limits' => 'required|array', 'limits.memory' => $rules['memory'], 'limits.swap' => $rules['swap'], @@ -34,26 +28,21 @@ class StoreServerRequest extends ApplicationApiRequest 'limits.io' => $rules['io'], 'limits.threads' => $rules['threads'], 'limits.cpu' => $rules['cpu'], + 'limits.oom_killer' => 'required|boolean', - // Application Resource Limits 'feature_limits' => 'required|array', - 'feature_limits.databases' => $rules['database_limit'], 'feature_limits.allocations' => $rules['allocation_limit'], 'feature_limits.backups' => $rules['backup_limit'], + 'feature_limits.databases' => $rules['database_limit'], - // Placeholders for rules added in withValidator() function. - 'allocation.default' => '', - 'allocation.additional.*' => '', + 'allocation.default' => 'required|bail|integer|exists:allocations,id', + 'allocation.additional.*' => 'integer|exists:allocations,id', - // Automatic deployment rules - 'deploy' => 'sometimes|required|array', - 'deploy.locations' => 'array', - 'deploy.locations.*' => 'integer|min:1', - 'deploy.dedicated_ip' => 'required_with:deploy,boolean', - 'deploy.port_range' => 'array', - 'deploy.port_range.*' => 'string', - - 'start_on_completion' => 'sometimes|boolean', + 'startup' => $rules['startup'], + 'environment' => 'present|array', + 'egg_id' => $rules['egg_id'], + 'image' => $rules['image'], + 'skip_scripts' => 'present|boolean', ]; } @@ -65,69 +54,30 @@ class StoreServerRequest extends ApplicationApiRequest 'external_id' => array_get($data, 'external_id'), 'name' => array_get($data, 'name'), 'description' => array_get($data, 'description'), - 'owner_id' => array_get($data, 'user'), - 'egg_id' => array_get($data, 'egg'), - 'image' => array_get($data, 'docker_image'), - 'startup' => array_get($data, 'startup'), - 'environment' => array_get($data, 'environment'), + 'owner_id' => array_get($data, 'owner_id'), + 'node_id' => array_get($data, 'node_id'), + 'memory' => array_get($data, 'limits.memory'), 'swap' => array_get($data, 'limits.swap'), 'disk' => array_get($data, 'limits.disk'), 'io' => array_get($data, 'limits.io'), - 'cpu' => array_get($data, 'limits.cpu'), 'threads' => array_get($data, 'limits.threads'), - 'skip_scripts' => array_get($data, 'skip_scripts', false), - 'allocation_id' => array_get($data, 'allocation.default'), - 'allocation_additional' => array_get($data, 'allocation.additional'), - 'start_on_completion' => array_get($data, 'start_on_completion', false), - 'database_limit' => array_get($data, 'feature_limits.databases'), + 'cpu' => array_get($data, 'limits.cpu'), + 'oom_disabled' => !array_get($data, 'limits.oom_killer'), + 'allocation_limit' => array_get($data, 'feature_limits.allocations'), 'backup_limit' => array_get($data, 'feature_limits.backups'), + 'database_limit' => array_get($data, 'feature_limits.databases'), + + 'allocation_id' => array_get($data, 'allocation.default'), + 'allocation_additional' => array_get($data, 'allocation.additional'), + + 'startup' => array_get($data, 'startup'), + 'environment' => array_get($data, 'environment'), + 'egg_id' => array_get($data, 'egg'), + 'image' => array_get($data, 'image'), + 'skip_scripts' => array_get($data, 'skip_scripts'), + 'start_on_completion' => array_get($data, 'start_on_completion', false), ]; } - - public function withValidator(Validator $validator) - { - $validator->sometimes('allocation.default', [ - 'required', - 'integer', - 'bail', - Rule::exists('allocations', 'id')->where(function ($query) { - $query->whereNull('server_id'); - }), - ], function ($input) { - return !($input->deploy); - }); - - $validator->sometimes('allocation.additional.*', [ - 'integer', - Rule::exists('allocations', 'id')->where(function ($query) { - $query->whereNull('server_id'); - }), - ], function ($input) { - return !($input->deploy); - }); - - $validator->sometimes('deploy.locations', 'present', function ($input) { - return $input->deploy; - }); - - $validator->sometimes('deploy.port_range', 'present', function ($input) { - return $input->deploy; - }); - } - - public function getDeploymentObject(): ?DeploymentObject - { - if (is_null($this->input('deploy'))) { - return null; - } - - $object = new DeploymentObject(); - $object->setDedicated($this->input('deploy.dedicated_ip', false)); - $object->setLocations($this->input('deploy.locations', [])); - $object->setPorts($this->input('deploy.port_range', [])); - - return $object; - } } diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php index 37637d664..9b254d047 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerRequest.php @@ -55,7 +55,7 @@ class UpdateServerRequest extends ApplicationApiRequest 'io' => array_get($data, 'limits.io'), 'threads' => array_get($data, 'limits.threads'), 'cpu' => array_get($data, 'limits.cpu'), - 'oom_disabled' => array_get($data, 'limits.oom_disabled'), + 'oom_disabled' => !array_get($data, 'limits.oom_killer'), 'allocation_limit' => array_get($data, 'feature_limits.allocations'), 'backup_limit' => array_get($data, 'feature_limits.backups'), diff --git a/resources/scripts/api/admin/node.ts b/resources/scripts/api/admin/node.ts index 3320e746a..e92a31cb8 100644 --- a/resources/scripts/api/admin/node.ts +++ b/resources/scripts/api/admin/node.ts @@ -1,6 +1,6 @@ import { Model, UUID, WithRelationships, withRelationships } from '@/api/admin/index'; import { Location } from '@/api/admin/location'; -import http from '@/api/http'; +import http, { QueryBuilderParams, withQueryBuilderParams } from '@/api/http'; import { AdminTransformers } from '@/api/admin/transformers'; import { Server } from '@/api/admin/server'; @@ -66,3 +66,11 @@ export const getNode = async (id: string | number): Promise): Promise => { + const { data } = await http.get('/api/application/nodes', { + params: withQueryBuilderParams(params), + }); + + return data.data.map(AdminTransformers.toNode); +}; diff --git a/resources/scripts/api/admin/server.ts b/resources/scripts/api/admin/server.ts index 368175543..3d0cc1524 100644 --- a/resources/scripts/api/admin/server.ts +++ b/resources/scripts/api/admin/server.ts @@ -79,11 +79,11 @@ type LoadedServer = WithRelationships; export const getServer = async (id: number | string): Promise => { const { data } = await http.get(`/api/application/servers/${id}`, { params: { - include: [ 'allocations', 'user', 'node' ], + include: [ 'allocations', 'user', 'node', 'variables' ], }, }); - return withRelationships(AdminTransformers.toServer(data), 'allocations', 'user', 'node'); + return withRelationships(AdminTransformers.toServer(data), 'allocations', 'user', 'node', 'variables'); }; /** diff --git a/resources/scripts/api/admin/servers/createServer.ts b/resources/scripts/api/admin/servers/createServer.ts index 88021de80..5ff75bc7e 100644 --- a/resources/scripts/api/admin/servers/createServer.ts +++ b/resources/scripts/api/admin/servers/createServer.ts @@ -1,57 +1,50 @@ import http from '@/api/http'; import { Server, rawDataToServer } from '@/api/admin/servers/getServers'; -interface CreateServerRequest { +export interface CreateServerRequest { + externalId: string; name: string; description: string | null; - user: number; - egg: number; - dockerImage: string; - startup: string; - skipScripts: boolean; - oomDisabled: boolean; - startOnCompletion: boolean; - environment: string[]; - - allocation: { - default: number; - additional: number[]; - }; + ownerId: number; + nodeId: number; limits: { - cpu: number; - disk: number; - io: number; memory: number; swap: number; + disk: number; + io: number; + cpu: number; threads: string; - }; + oomDisabled: boolean; + } featureLimits: { allocations: number; backups: number; databases: number; }; + + allocation: { + default: number; + additional: number[]; + }; + + startup: string; + environment: Record; + eggId: number; + image: string; + skipScripts: boolean; + startOnCompletion: boolean; } export default (r: CreateServerRequest, include: string[] = []): Promise => { return new Promise((resolve, reject) => { http.post('/api/application/servers', { + externalId: r.externalId, name: r.name, description: r.description, - user: r.user, - egg: r.egg, - docker_image: r.dockerImage, - startup: r.startup, - skip_scripts: r.skipScripts, - oom_disabled: r.oomDisabled, - start_on_completion: r.startOnCompletion, - environment: r.environment, - - allocation: { - default: r.allocation.default, - additional: r.allocation.additional, - }, + owner_id: r.ownerId, + node_id: r.nodeId, limits: { cpu: r.limits.cpu, @@ -67,6 +60,18 @@ export default (r: CreateServerRequest, include: string[] = []): Promise backups: r.featureLimits.backups, databases: r.featureLimits.databases, }, + + allocation: { + default: r.allocation.default, + additional: r.allocation.additional, + }, + + startup: r.startup, + environment: r.environment, + egg_id: r.eggId, + image: r.image, + skip_scripts: r.skipScripts, + start_on_completion: r.startOnCompletion, }, { params: { include: include.join(',') } }) .then(({ data }) => resolve(rawDataToServer(data))) .catch(reject); diff --git a/resources/scripts/api/admin/servers/updateServer.ts b/resources/scripts/api/admin/servers/updateServer.ts index b8c59d6cc..e74b7422a 100644 --- a/resources/scripts/api/admin/servers/updateServer.ts +++ b/resources/scripts/api/admin/servers/updateServer.ts @@ -43,7 +43,7 @@ export default (id: number, server: Partial, include: string[] = []): Pr io: server.limits?.io, cpu: server.limits?.cpu, threads: server.limits?.threads, - oom_disabled: server.limits?.oomDisabled, + oom_killer: server.limits?.oomDisabled, }, feature_limits: { diff --git a/resources/scripts/components/admin/servers/EggSelect.tsx b/resources/scripts/components/admin/servers/EggSelect.tsx index 2d708ba36..00aabd7d1 100644 --- a/resources/scripts/components/admin/servers/EggSelect.tsx +++ b/resources/scripts/components/admin/servers/EggSelect.tsx @@ -1,9 +1,9 @@ -import Label from '@/components/elements/Label'; -import Select from '@/components/elements/Select'; import { useField } from 'formik'; import React, { useEffect, useState } from 'react'; -import { Egg, searchEggs } from '@/api/admin/egg'; import { WithRelationships } from '@/api/admin'; +import { Egg, searchEggs } from '@/api/admin/egg'; +import Label from '@/components/elements/Label'; +import Select from '@/components/elements/Select'; interface Props { nestId?: number; @@ -15,31 +15,40 @@ export default ({ nestId, selectedEggId, onEggSelect }: Props) => { const [ , , { setValue, setTouched } ] = useField>('environment'); const [ eggs, setEggs ] = useState[] | null>(null); - useEffect(() => { - if (!nestId) return setEggs(null); + const selectEgg = (egg: Egg | null) => { + if (egg === null) { + onEggSelect(null); + return; + } - searchEggs(nestId, {}).then(eggs => { - setEggs(eggs); - onEggSelect(eggs[0] || null); - }).catch(error => console.error(error)); + // Clear values + setValue({}); + setTouched(true); + + onEggSelect(egg); + + const values: Record = {}; + egg.relationships.variables?.forEach(v => { values[v.environmentVariable] = v.defaultValue; }); + setValue(values); + setTouched(true); + }; + + useEffect(() => { + if (!nestId) { + setEggs(null); + return; + } + + searchEggs(nestId, {}) + .then(eggs => { + setEggs(eggs); + selectEgg(eggs[0] || null); + }) + .catch(error => console.error(error)); }, [ nestId ]); const onSelectChange = (e: React.ChangeEvent) => { - if (!eggs) return; - - const match = eggs.find(egg => String(egg.id) === e.currentTarget.value); - if (!match) return onEggSelect(null); - - // Ensure that only new egg variables are present in the record storing all - // of the possible variables. This ensures the fields are controlled, rather - // than uncontrolled when a user begins typing in them. - setValue(match.relationships.variables.reduce((obj, value) => ({ - ...obj, - [value.environmentVariable]: undefined, - }), {})); - setTouched(true); - - onEggSelect(match); + selectEgg(eggs?.find(egg => egg.id.toString() === e.currentTarget.value) || null); }; return ( diff --git a/resources/scripts/components/admin/servers/NewServerContainer.tsx b/resources/scripts/components/admin/servers/NewServerContainer.tsx index 61abd51dc..f17af9f5e 100644 --- a/resources/scripts/components/admin/servers/NewServerContainer.tsx +++ b/resources/scripts/components/admin/servers/NewServerContainer.tsx @@ -1,28 +1,117 @@ import { Egg } from '@/api/admin/egg'; import AdminBox from '@/components/admin/AdminBox'; +import NodeSelect from '@/components/admin/servers/NodeSelect'; import { ServerImageContainer, ServerServiceContainer, ServerVariableContainer } from '@/components/admin/servers/ServerStartupContainer'; import BaseSettingsBox from '@/components/admin/servers/settings/BaseSettingsBox'; import FeatureLimitsBox from '@/components/admin/servers/settings/FeatureLimitsBox'; import ServerResourceBox from '@/components/admin/servers/settings/ServerResourceBox'; import Button from '@/components/elements/Button'; import Field from '@/components/elements/Field'; +import FormikSwitch from '@/components/elements/FormikSwitch'; import Label from '@/components/elements/Label'; import Select from '@/components/elements/Select'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import FlashMessageRender from '@/components/FlashMessageRender'; import { faNetworkWired } from '@fortawesome/free-solid-svg-icons'; -import { Form, Formik } from 'formik'; +import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import React, { useState } from 'react'; import tw from 'twin.macro'; import AdminContentBlock from '@/components/admin/AdminContentBlock'; import { object } from 'yup'; -import { Values } from '@/api/admin/servers/updateServer'; +import { CreateServerRequest } from '@/api/admin/servers/createServer'; + +function InternalForm () { + const { isSubmitting, isValid, values: { environment } } = useFormikContext(); -export default () => { const [ egg, setEgg ] = useState(null); - const submit = (_: Values) => { - // + return ( +
+
+
+ + +
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+ + + + + + + +
+ {/* This ensures that no variables are rendered unless the environment has a value for the variable. */} + {egg?.relationships.variables?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined).map((v, i) => ( + + ))} +
+ +
+
+ +
+
+
+
+ ); +} + +export default () => { + const submit = (r: CreateServerRequest, { setSubmitting }: FormikHelpers) => { + console.log(r); + setSubmitting(false); }; return ( @@ -41,12 +130,14 @@ export default () => { initialValues={{ externalId: '', name: '', + description: '', ownerId: 0, + nodeId: 0, limits: { - memory: 0, + memory: 1024, swap: 0, - disk: 0, - io: 0, + disk: 4096, + io: 500, cpu: 0, threads: '', // This value is inverted to have the switch be on when the @@ -58,82 +149,20 @@ export default () => { backups: 0, databases: 0, }, - allocationId: 0, - addAllocations: [] as number[], - removeAllocations: [] as number[], - }} + allocation: { + default: 0, + additional: [] as number[], + }, + startup: '', + environment: [], + eggId: 0, + image: '', + skipScripts: false, + startOnCompletion: true, + } as CreateServerRequest} validationSchema={object().shape({})} > - {({ isSubmitting, isValid }) => ( -
-
-
- - - {/* TODO: in networking box only show primary allocation and additional allocations */} - {/* TODO: add node select */} - -
-
- -
-
- - -
-
-
- - -
- - - - - - - -
- {egg?.relationships.variables?.map((v, i) => ( - - ))} -
- -
-
- -
-
-
-
- )} + ); diff --git a/resources/scripts/components/admin/servers/NodeSelect.tsx b/resources/scripts/components/admin/servers/NodeSelect.tsx new file mode 100644 index 000000000..63508be66 --- /dev/null +++ b/resources/scripts/components/admin/servers/NodeSelect.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { useFormikContext } from 'formik'; +import SearchableSelect, { Option } from '@/components/elements/SearchableSelect'; +import { Node, searchNodes } from '@/api/admin/node'; + +export default ({ selected }: { selected?: Node }) => { + const context = useFormikContext(); + + const [ node, setNode ] = useState(selected || null); + const [ nodes, setNodes ] = useState(null); + + const onSearch = async (query: string) => { + setNodes( + await searchNodes({ filters: { name: query } }), + ); + }; + + const onSelect = (node: Node | null) => { + setNode(node); + context.setFieldValue('ownerId', node?.id || null); + }; + + const getSelectedText = (node: Node | null): string => node?.name || ''; + + return ( + + {nodes?.map(d => ( + + ))} + + ); +}; diff --git a/resources/scripts/components/admin/servers/ServerStartupContainer.tsx b/resources/scripts/components/admin/servers/ServerStartupContainer.tsx index 76ff3c541..0a5039034 100644 --- a/resources/scripts/components/admin/servers/ServerStartupContainer.tsx +++ b/resources/scripts/components/admin/servers/ServerStartupContainer.tsx @@ -10,7 +10,7 @@ import AdminBox from '@/components/admin/AdminBox'; import tw from 'twin.macro'; import Field from '@/components/elements/Field'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; +import { Form, Formik, FormikHelpers, useField, useFormikContext } from 'formik'; import { ApplicationStore } from '@/state'; import { Actions, useStoreActions } from 'easy-peasy'; import Label from '@/components/elements/Label'; @@ -60,7 +60,7 @@ function ServerStartupLineContainer ({ egg, server }: { egg: Egg | null; server: export function ServerServiceContainer ({ egg, setEgg, nestId: _nestId }: { egg: Egg | null, setEgg: (value: Egg | null) => void, nestId: number }) { const { isSubmitting } = useFormikContext(); - const [ nestId, setNestId ] = useState(_nestId); + const [ nestId, setNestId ] = useState(_nestId); return ( @@ -71,7 +71,7 @@ export function ServerServiceContainer ({ egg, setEgg, nestId: _nestId }: { egg:
- +
); @@ -98,14 +98,21 @@ export function ServerImageContainer () { ); } -export function ServerVariableContainer ({ variable, defaultValue }: { variable: EggVariable, defaultValue: string }) { +export function ServerVariableContainer ({ variable, value }: { variable: EggVariable, value?: string }) { const key = 'environment.' + variable.environmentVariable; - const { isSubmitting, setFieldValue } = useFormikContext(); + const [ , , { setValue, setTouched } ] = useField(key); + + const { isSubmitting } = useFormikContext(); useEffect(() => { - setFieldValue(key, defaultValue); - }, [ variable, defaultValue ]); + if (value === undefined) { + return; + } + + setValue(value); + setTouched(true); + }, [ value ]); return ( {variable.name}

}> @@ -123,7 +130,7 @@ export function ServerVariableContainer ({ variable, defaultValue }: { variable: } function ServerStartupForm ({ egg, setEgg, server }: { egg: Egg | null, setEgg: (value: Egg | null) => void; server: Server }) { - const { isSubmitting, isValid } = useFormikContext(); + const { isSubmitting, isValid, values: { environment } } = useFormikContext(); return (
@@ -150,11 +157,12 @@ function ServerStartupForm ({ egg, setEgg, server }: { egg: Egg | null, setEgg:
- {egg?.relationships.variables?.map((v, i) => ( + {/* This ensures that no variables are rendered unless the environment has a value for the variable. */} + {egg?.relationships.variables?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined).map((v, i) => ( v.eggId === v2.eggId && v.environmentVariable === v2.environmentVariable)?.serverValue || v.defaultValue} + value={server.relationships.variables?.find(v2 => v.eggId === v2.eggId && v.environmentVariable === v2.environmentVariable)?.serverValue} /> ))}
@@ -205,14 +213,12 @@ export default () => { onSubmit={submit} initialValues={{ startup: server.container.startup, - // Don't ask. - environment: Object.fromEntries(egg?.relationships.variables.map(v => [ v.environmentVariable, '' ]) || []), + environment: [] as Record, image: server.container.image, eggId: server.eggId, skipScripts: false, }} - validationSchema={object().shape({ - })} + validationSchema={object().shape({})} > { +export default ({ children }: { children?: ReactNode }) => { const { data: server } = useServerFromRoute(); const { isSubmitting } = useFormikContext(); return (
- + + {children}
);