From 5e99bb8dd6edc5b45fe07c0690f2e6265d60dda5 Mon Sep 17 00:00:00 2001
From: Matthew Penner
Date: Sun, 24 Oct 2021 14:14:04 -0600
Subject: [PATCH] ui(admin): fix server startup variables
---
.../Servers/StoreServerRequest.php | 106 +++-------
.../Servers/UpdateServerRequest.php | 2 +-
resources/scripts/api/admin/node.ts | 10 +-
resources/scripts/api/admin/server.ts | 4 +-
.../scripts/api/admin/servers/createServer.ts | 67 +++---
.../scripts/api/admin/servers/updateServer.ts | 2 +-
.../components/admin/servers/EggSelect.tsx | 57 +++---
.../admin/servers/NewServerContainer.tsx | 193 ++++++++++--------
.../components/admin/servers/NodeSelect.tsx | 47 +++++
.../admin/servers/ServerStartupContainer.tsx | 34 +--
.../servers/settings/BaseSettingsBox.tsx | 7 +-
11 files changed, 292 insertions(+), 237 deletions(-)
create mode 100644 resources/scripts/components/admin/servers/NodeSelect.tsx
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 (
+
+ );
+}
+
+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 }) => (
-
- )}
+
);
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 (