ui(admin): server edit cleanup, fix startup form

This commit is contained in:
Matthew Penner 2021-09-16 14:59:22 -06:00
parent 95f3eb54db
commit df895f4a9f
No known key found for this signature in database
GPG key ID: 030E4AB751DC756F
12 changed files with 422 additions and 322 deletions

View file

@ -16,18 +16,20 @@ class UpdateServerRequest extends ApplicationApiRequest
'name' => $rules['name'],
'description' => array_merge(['nullable'], $rules['description']),
'owner_id' => $rules['owner_id'],
'oom_killer' => 'sometimes|boolean',
'memory' => $rules['memory'],
'swap' => $rules['swap'],
'disk' => $rules['disk'],
'io' => $rules['io'],
'threads' => $rules['threads'],
'cpu' => $rules['cpu'],
'limits' => 'sometimes|array',
'limits.memory' => $rules['memory'],
'limits.swap' => $rules['swap'],
'limits.disk' => $rules['disk'],
'limits.io' => $rules['io'],
'limits.threads' => $rules['threads'],
'limits.cpu' => $rules['cpu'],
'limits.oom_killer' => 'sometimes|boolean',
'databases' => $rules['database_limit'],
'allocations' => $rules['allocation_limit'],
'backups' => $rules['backup_limit'],
'feature_limits' => 'required|array',
'feature_limits.allocations' => $rules['allocation_limit'],
'feature_limits.backups' => $rules['backup_limit'],
'feature_limits.databases' => $rules['database_limit'],
'allocation_id' => 'bail|exists:allocations,id',
'add_allocations' => 'bail|array',
@ -46,18 +48,18 @@ class UpdateServerRequest extends ApplicationApiRequest
'name' => array_get($data, 'name'),
'description' => array_get($data, 'description'),
'owner_id' => array_get($data, 'owner_id'),
'oom_disabled' => !array_get($data, 'oom_killer'),
'memory' => array_get($data, 'memory'),
'swap' => array_get($data, 'swap'),
'disk' => array_get($data, 'disk'),
'io' => array_get($data, 'io'),
'threads' => array_get($data, 'threads'),
'cpu' => array_get($data, 'cpu'),
'memory' => array_get($data, 'limits.memory'),
'swap' => array_get($data, 'limits.swap'),
'disk' => array_get($data, 'limits.disk'),
'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'),
'database_limit' => array_get($data, 'databases'),
'allocation_limit' => array_get($data, 'allocations'),
'backup_limit' => array_get($data, 'backups'),
'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_id'),
'add_allocations' => array_get($data, 'add_allocations'),

View file

@ -9,24 +9,14 @@ class UpdateServerStartupRequest extends ApplicationApiRequest
{
public function rules(): array
{
$data = Server::getRulesForUpdate($this->route()->parameter('server')->id);
$rules = Server::getRulesForUpdate($this->route()->parameter('server')->id);
return [
'startup' => $data['startup'],
'startup' => $rules['startup'],
'environment' => 'present|array',
'egg' => $data['egg_id'],
'image' => $data['image'],
'egg_id' => $rules['egg_id'],
'image' => $rules['image'],
'skip_scripts' => 'present|boolean',
];
}
public function validated(): array
{
$data = parent::validated();
return collect($data)->only(['startup', 'environment', 'skip_scripts'])->merge([
'egg_id' => array_get($data, 'egg'),
'docker_image' => array_get($data, 'image'),
])->toArray();
}
}

View file

@ -69,7 +69,7 @@ class ServerTransformer extends Transformer
'nest_id' => $model->nest_id,
'egg_id' => $model->egg_id,
'container' => [
'startup_command' => $model->startup,
'startup' => $model->startup,
'image' => $model->image,
'environment' => $this->environmentService->handle($model),
],

View file

@ -71,8 +71,7 @@ export interface Server {
eggId: number;
container: {
startupCommand: string;
defaultStartup: string;
startup: string;
image: string;
environment: Map<string, string>;
}
@ -121,8 +120,7 @@ export const rawDataToServer = ({ attributes }: FractalResponseData): Server =>
eggId: attributes.egg_id,
container: {
startupCommand: attributes.container.startup_command,
defaultStartup: '',
startup: attributes.container.startup,
image: attributes.container.image,
environment: attributes.container.environment,
},

View file

@ -6,17 +6,21 @@ export interface Values {
name: string;
ownerId: number;
memory: number;
swap: number;
disk: number;
io: number;
cpu: number;
threads: string;
oomDisabled: boolean;
limits: {
memory: number;
swap: number;
disk: number;
io: number;
cpu: number;
threads: string;
oomDisabled: boolean;
}
databases: number;
allocations: number;
backups: number;
featureLimits: {
allocations: number;
backups: number;
databases: number;
}
allocationId: number;
addAllocations: number[];
@ -24,16 +28,36 @@ export interface Values {
}
export default (id: number, server: Partial<Values>, include: string[] = []): Promise<Server> => {
const data = {};
Object.keys(server).forEach((key) => {
const key2 = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
// @ts-ignore
data[key2] = server[key];
});
return new Promise((resolve, reject) => {
http.patch(`/api/application/servers/${id}`, data, { params: { include: include.join(',') } })
http.patch(
`/api/application/servers/${id}`,
{
external_id: server.externalId,
name: server.name,
owner_id: server.ownerId,
limits: {
memory: server.limits?.memory,
swap: server.limits?.swap,
disk: server.limits?.disk,
io: server.limits?.io,
cpu: server.limits?.cpu,
threads: server.limits?.threads,
oom_disabled: server.limits?.oomDisabled,
},
feature_limits: {
allocations: server.featureLimits?.allocations,
backups: server.featureLimits?.backups,
databases: server.featureLimits?.databases,
},
allocation_id: server.allocationId,
add_allocations: server.addAllocations,
remove_allocations: server.removeAllocations,
},
{ params: { include: include.join(',') } }
)
.then(({ data }) => resolve(rawDataToServer(data)))
.catch(reject);
});

View file

@ -0,0 +1,28 @@
import http from '@/api/http';
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
export interface Values {
startup: string;
environment: Record<string, any>;
eggId: number;
image: string;
skipScripts: boolean;
}
export default (id: number, values: Partial<Values>, include: string[] = []): Promise<Server> => {
return new Promise((resolve, reject) => {
http.patch(
`/api/application/servers/${id}/startup`,
{
startup: values.startup,
environment: values.environment,
egg_id: values.eggId,
image: values.image,
skip_scripts: values.skipScripts,
},
{ params: { include: include.join(',') } }
)
.then(({ data }) => resolve(rawDataToServer(data)))
.catch(reject);
});
};

View file

@ -1,12 +1,39 @@
import Label from '@/components/elements/Label';
import Select from '@/components/elements/Select';
import { useFormikContext } from 'formik';
import React, { useEffect, useState } from 'react';
import { Egg } from '@/api/admin/eggs/getEgg';
import searchEggs from '@/api/admin/nests/searchEggs';
export default ({ nestId, egg, setEgg }: { nestId: number | null; egg: Egg | null, setEgg: (value: Egg | null) => void }) => {
const { setFieldValue } = useFormikContext();
const [ eggs, setEggs ] = useState<Egg[]>([]);
/**
* So you may be asking yourself, "what cluster-fuck of code is this?"
*
* Well, this code makes sure that when the egg changes, that the environment
* object has empty string values instead of undefined so React doesn't think
* the variable fields are uncontrolled.
*/
const setEgg2 = (newEgg: Egg | null) => {
if (newEgg === null) {
setEgg(null);
return;
}
// Reset all variables to be empty, don't inherit the previous values.
const newVariables = newEgg?.relations.variables;
newVariables?.forEach(v => setFieldValue('environment.' + v.envVariable, ''));
const variables = egg?.relations.variables?.filter(v => newVariables?.find(v2 => v2.envVariable === v.envVariable) === undefined);
setEgg(newEgg);
// Clear any variables that don't exist on the new egg.
variables?.forEach(v => setFieldValue('environment.' + v.envVariable, undefined));
};
useEffect(() => {
if (nestId === null) {
return;
@ -16,10 +43,10 @@ export default ({ nestId, egg, setEgg }: { nestId: number | null; egg: Egg | nul
.then(eggs => {
setEggs(eggs);
if (eggs.length < 1) {
setEgg(null);
setEgg2(null);
return;
}
setEgg(eggs[0]);
setEgg2(eggs[0]);
})
.catch(error => console.error(error));
}, [ nestId ]);
@ -31,7 +58,7 @@ export default ({ nestId, egg, setEgg }: { nestId: number | null; egg: Egg | nul
defaultValue={egg?.id || undefined}
id={'eggId'}
name={'eggId'}
onChange={e => setEgg(eggs.find(egg => egg.id.toString() === e.currentTarget.value) || null)}
onChange={e => setEgg2(eggs.find(egg => egg.id.toString() === e.currentTarget.value) || null)}
>
{eggs.map(v => (
<option key={v.id} value={v.id.toString()}>

View file

@ -14,6 +14,8 @@ import { ApplicationStore } from '@/state';
import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation';
import ServerSettingsContainer from '@/components/admin/servers/ServerSettingsContainer';
export const ServerIncludes = [ 'allocations', 'user', 'variables' ];
interface ctx {
server: Server | undefined;
setServer: Action<ctx, Server | undefined>;
@ -40,7 +42,7 @@ const ServerRouter = () => {
useEffect(() => {
clearFlashes('server');
getServer(Number(match.params?.id), [ 'allocations', 'user', 'variables' ])
getServer(Number(match.params?.id), ServerIncludes)
.then(server => setServer(server))
.catch(error => {
console.error(error);

View file

@ -14,140 +14,13 @@ import updateServer, { Values } 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 { Context, ServerIncludes } 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';
export function ServerFeatureContainer () {
const { isSubmitting } = useFormikContext();
return (
<AdminBox icon={faConciergeBell} title={'Feature Limits'} css={tw`relative w-full`}>
<SpinnerOverlay visible={isSubmitting}/>
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
<Field
id={'databases'}
name={'databases'}
label={'Database Limit'}
type={'number'}
description={'The total number of databases a user is allowed to create for this server.'}
/>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mx-4 md:mb-0`}>
<Field
id={'allocations'}
name={'allocations'}
label={'Allocation Limit'}
type={'number'}
description={'The total number of allocations a user is allowed to create for this server.'}
/>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
<Field
id={'backups'}
name={'backups'}
label={'Backup Limit'}
type={'number'}
description={'The total number of backups that can be created for this server.'}
/>
</div>
</div>
</AdminBox>
);
}
export function ServerResourceContainer () {
const { isSubmitting } = useFormikContext();
return (
<AdminBox icon={faBalanceScale} title={'Resources'} css={tw`relative w-full`}>
<SpinnerOverlay visible={isSubmitting}/>
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
<Field
id={'cpu'}
name={'cpu'}
label={'CPU Limit'}
type={'text'}
description={'Each thread on the system is considered to be 100%. Setting this value to 0 will allow the server to use CPU time without restriction.'}
/>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
<Field
id={'threads'}
name={'threads'}
label={'CPU Pinning'}
type={'text'}
description={'Advanced: Enter the specific CPU cores that this server can run on, or leave blank to allow all cores. This can be a single number, and or a comma seperated list, and or a dashed range. Example: 0, 0-1,3, or 0,1,3,4. It is recommended to leave this value blank and let the CPU handle balancing the load.'}
/>
</div>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
<Field
id={'memory'}
name={'memory'}
label={'Memory Limit'}
type={'number'}
description={'The maximum amount of memory allowed for this container. Setting this to 0 will allow unlimited memory in a container.'}
/>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
<Field
id={'swap'}
name={'swap'}
label={'Swap Limit'}
type={'number'}
/>
</div>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
<Field
id={'disk'}
name={'disk'}
label={'Disk Limit'}
type={'number'}
description={'This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to 0 to allow unlimited disk usage.'}
/>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
<Field
id={'io'}
name={'io'}
label={'Block IO Proportion'}
type={'number'}
description={'Advanced: The IO performance of this server relative to other running containers on the system. Value should be between 10 and 1000.'}
/>
</div>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
<div css={tw`bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
<FormikSwitch
name={'oomKiller'}
label={'Out of Memory Killer'}
description={'Enabling the Out of Memory Killer may cause server processes to exit unexpectedly.'}
/>
</div>
</div>
</AdminBox>
);
}
export function ServerSettingsContainer ({ server }: { server?: Server }) {
const { isSubmitting } = useFormikContext();
@ -184,6 +57,42 @@ export function ServerSettingsContainer ({ server }: { server?: Server }) {
);
}
export function ServerFeatureContainer () {
const { isSubmitting } = useFormikContext();
return (
<AdminBox icon={faConciergeBell} title={'Feature Limits'} css={tw`relative w-full`}>
<SpinnerOverlay visible={isSubmitting}/>
<div css={tw`grid grid-cols-1 md:grid-cols-3 gap-x-8 gap-y-6`}>
<Field
id={'featureLimits.allocations'}
name={'featureLimits.allocations'}
label={'Allocation Limit'}
type={'number'}
description={'The total number of allocations a user is allowed to create for this server.'}
/>
<Field
id={'featureLimits.backups'}
name={'featureLimits.backups'}
label={'Backup Limit'}
type={'number'}
description={'The total number of backups that can be created for this server.'}
/>
<Field
id={'featureLimits.databases'}
name={'featureLimits.databases'}
label={'Database Limit'}
type={'number'}
description={'The total number of databases a user is allowed to create for this server.'}
/>
</div>
</AdminBox>
);
}
export function ServerAllocationsContainer ({ server }: { server: Server }) {
const { isSubmitting } = useFormikContext();
@ -234,7 +143,90 @@ export function ServerAllocationsContainer ({ server }: { server: Server }) {
);
}
type Values2 = Omit<Values, 'oomDisabled'> & { oomKiller: boolean };
export function ServerResourceContainer () {
const { isSubmitting } = useFormikContext();
return (
<AdminBox icon={faBalanceScale} title={'Resources'} css={tw`relative w-full`}>
<SpinnerOverlay visible={isSubmitting}/>
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
<Field
id={'limits.cpu'}
name={'limits.cpu'}
label={'CPU Limit'}
type={'text'}
description={'Each thread on the system is considered to be 100%. Setting this value to 0 will allow the server to use CPU time without restriction.'}
/>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
<Field
id={'limits.threads'}
name={'limits.threads'}
label={'CPU Pinning'}
type={'text'}
description={'Advanced: Enter the specific CPU cores that this server can run on, or leave blank to allow all cores. This can be a single number, and or a comma seperated list, and or a dashed range. Example: 0, 0-1,3, or 0,1,3,4. It is recommended to leave this value blank and let the CPU handle balancing the load.'}
/>
</div>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
<Field
id={'limits.memory'}
name={'limits.memory'}
label={'Memory Limit'}
type={'number'}
description={'The maximum amount of memory allowed for this container. Setting this to 0 will allow unlimited memory in a container.'}
/>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
<Field
id={'limits.swap'}
name={'limits.swap'}
label={'Swap Limit'}
type={'number'}
/>
</div>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:mr-4 md:mb-0`}>
<Field
id={'limits.disk'}
name={'limits.disk'}
label={'Disk Limit'}
type={'number'}
description={'This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to 0 to allow unlimited disk usage.'}
/>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-col md:ml-4 md:mb-0`}>
<Field
id={'limits.io'}
name={'limits.io'}
label={'Block IO Proportion'}
type={'number'}
description={'Advanced: The IO performance of this server relative to other running containers on the system. Value should be between 10 and 1000.'}
/>
</div>
</div>
<div css={tw`mb-6 md:w-full md:flex md:flex-row`}>
<div css={tw`bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
<FormikSwitch
name={'limits.oomDisabled'}
label={'Out of Memory Killer'}
description={'Enabling the Out of Memory Killer may cause server processes to exit unexpectedly.'}
/>
</div>
</div>
</AdminBox>
);
}
export default function ServerSettingsContainer2 ({ server }: { server: Server }) {
const history = useHistory();
@ -243,10 +235,14 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
const setServer = Context.useStoreActions(actions => actions.setServer);
const submit = (values: Values2, { setSubmitting, setFieldValue }: FormikHelpers<Values2>) => {
const submit = (values: Values, { setSubmitting, setFieldValue }: FormikHelpers<Values>) => {
clearFlashes('server');
updateServer(server.id, { ...values, oomDisabled: !values.oomKiller }, [ 'allocations', 'user' ])
// This value is inverted to have the switch be on when the
// OOM Killer is enabled, rather than when disabled.
values.limits.oomDisabled = !values.limits.oomDisabled;
updateServer(server.id, values, ServerIncludes)
.then(s => {
setServer({ ...server, ...s });
@ -269,19 +265,23 @@ export default function ServerSettingsContainer2 ({ server }: { server: Server }
name: server.name,
ownerId: server.ownerId,
memory: server.limits.memory,
swap: server.limits.swap,
disk: server.limits.disk,
io: server.limits.io,
cpu: server.limits.cpu,
threads: server.limits.threads || '',
// Yes, this is named differently on purpose. Naming it like this makes the toggle switch
// be in an ON state when the oom killer is enabled, instead of when its disabled.
oomKiller: !server.limits.oomDisabled,
limits: {
memory: server.limits.memory,
swap: server.limits.swap,
disk: server.limits.disk,
io: server.limits.io,
cpu: server.limits.cpu,
threads: server.limits.threads || '',
// This value is inverted to have the switch be on when the
// OOM Killer is enabled, rather than when disabled.
oomDisabled: !server.limits.oomDisabled,
},
databases: server.featureLimits.databases,
allocations: server.featureLimits.allocations,
backups: server.featureLimits.backups,
featureLimits: {
allocations: server.featureLimits.allocations,
backups: server.featureLimits.backups,
databases: server.featureLimits.databases,
},
allocationId: server.allocationId,
addAllocations: [] as number[],

View file

@ -1,30 +1,22 @@
import getEgg, { Egg, EggVariable } from '@/api/admin/eggs/getEgg';
import { Server } from '@/api/admin/servers/getServers';
import updateServerStartup, { Values } from '@/api/admin/servers/updateServerStartup';
import EggSelect from '@/components/admin/servers/EggSelect';
import NestSelect from '@/components/admin/servers/NestSelect';
import { Context, ServerIncludes } from '@/components/admin/servers/ServerRouter';
import FormikSwitch from '@/components/elements/FormikSwitch';
import InputSpinner from '@/components/elements/InputSpinner';
import React, { useEffect, useState } from 'react';
import Button from '@/components/elements/Button';
import Input from '@/components/elements/Input';
import AdminBox from '@/components/admin/AdminBox';
import tw from 'twin.macro';
import { object } from 'yup';
import Field from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { Form, Formik, useFormikContext } from 'formik';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { ApplicationStore } from '@/state';
import { Actions, useStoreActions } from 'easy-peasy';
import Label from '@/components/elements/Label';
import TitledGreyBox from '@/components/elements/TitledGreyBox';
// interface Values {
// startupCommand: string;
// image: string;
//
// eggId: number;
// skipScripts: boolean;
// }
import { object } from 'yup';
function ServerStartupLineContainer ({ egg }: { egg: Egg }) {
const { isSubmitting } = useFormikContext();
@ -33,22 +25,20 @@ function ServerStartupLineContainer ({ egg }: { egg: Egg }) {
<AdminBox title={'Startup Command'} css={tw`relative w-full`}>
<SpinnerOverlay visible={isSubmitting}/>
<Form css={tw`mb-0`}>
<div css={tw`mb-6`}>
<Field
id={'startupCommand'}
name={'startupCommand'}
label={'Startup Command'}
type={'text'}
description={'Edit your server\'s startup command here. The following variables are available by default: {{SERVER_MEMORY}}, {{SERVER_IP}}, and {{SERVER_PORT}}.'}
/>
</div>
<div css={tw`mb-6`}>
<Field
id={'startup'}
name={'startup'}
label={'Startup Command'}
type={'text'}
description={'Edit your server\'s startup command here. The following variables are available by default: {{SERVER_MEMORY}}, {{SERVER_IP}}, and {{SERVER_PORT}}.'}
/>
</div>
<div>
<Label>Default Startup Command</Label>
<Input value={egg.startup} readOnly/>
</div>
</Form>
<div>
<Label>Default Startup Command</Label>
<Input value={egg.startup} readOnly/>
</div>
</AdminBox>
);
}
@ -62,23 +52,21 @@ function ServerServiceContainer ({ server, egg, setEgg }: { server: Server, egg:
<AdminBox title={'Service Configuration'} css={tw`relative w-full`}>
<SpinnerOverlay visible={isSubmitting}/>
<Form css={tw`mb-0`}>
<div css={tw`mb-6`}>
<NestSelect nestId={nestId} setNestId={setNestId}/>
</div>
<div css={tw`mb-6`}>
<NestSelect nestId={nestId} setNestId={setNestId}/>
</div>
<div css={tw`mb-6`}>
<EggSelect nestId={nestId} egg={egg} setEgg={setEgg}/>
</div>
<div css={tw`mb-6`}>
<EggSelect nestId={nestId} egg={egg} setEgg={setEgg}/>
</div>
<div css={tw`bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
<FormikSwitch
name={'skipScript'}
label={'Skip Egg Install Script'}
description={'SoonTM'}
/>
</div>
</Form>
<div css={tw`bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
<FormikSwitch
name={'skipScript'}
label={'Skip Egg Install Script'}
description={'SoonTM'}
/>
</div>
</AdminBox>
);
}
@ -90,52 +78,119 @@ function ServerImageContainer () {
<AdminBox title={'Image Configuration'} css={tw`relative w-full`}>
<SpinnerOverlay visible={isSubmitting}/>
<Form css={tw`mb-0`}>
<div css={tw`md:w-full md:flex md:flex-col`}>
<div>
<Field
id={'image'}
name={'image'}
label={'Docker Image'}
type={'text'}
/>
</div>
<div css={tw`md:w-full md:flex md:flex-col`}>
<div>
<Field
id={'image'}
name={'image'}
label={'Docker Image'}
type={'text'}
/>
</div>
</Form>
</div>
</AdminBox>
);
}
function ServerVariableContainer ({ variable, defaultValue }: { variable: EggVariable, defaultValue: string }) {
const [ value, setValue ] = useState<string>('');
const key = 'environment.' + variable.envVariable;
const { isSubmitting, setFieldValue } = useFormikContext();
useEffect(() => {
setValue(defaultValue);
}, [ defaultValue ]);
setFieldValue(key, defaultValue);
// return () => {
// setFieldValue(key, undefined);
// };
}, [ variable, defaultValue ]);
return (
<AdminBox title={<p css={tw`text-sm uppercase`}>{variable.name}</p>}>
<InputSpinner visible={false}>
<Input
name={variable.envVariable}
placeholder={variable.defaultValue}
type={'text'}
value={value}
onChange={e => setValue(e.target.value)}
/>
</InputSpinner>
<p css={tw`mt-1 text-xs text-neutral-300`}>
{variable.description}
</p>
<AdminBox css={tw`relative w-full`} title={<p css={tw`text-sm uppercase`}>{variable.name}</p>}>
<SpinnerOverlay visible={isSubmitting}/>
<Field
id={key}
name={key}
type={'text'}
placeholder={variable.defaultValue}
description={variable.description}
/>
</AdminBox>
);
}
export default function ServerStartupContainer ({ server }: { server: Server }) {
const { clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
function ServerStartupForm ({ server }: { server: Server }) {
const { isSubmitting, isValid, setFieldValue } = useFormikContext();
const [ egg, setEgg ] = useState<Egg | null>(null);
useEffect(() => {
getEgg(server.eggId, [ 'variables' ])
.then(egg => {
if (egg.relations.variables === undefined) {
return;
}
egg.relations.variables?.forEach(v => setFieldValue('environment.' + v.envVariable, ''));
setEgg(egg);
})
.catch(error => console.error(error));
}, []);
if (egg === null) {
return (<></>);
}
return (
<Form>
<div css={tw`flex flex-col`}>
<div css={tw`flex flex-row mb-6`}>
<ServerStartupLineContainer egg={egg}/>
</div>
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6`}>
<div css={tw`flex`}>
<ServerServiceContainer
server={server}
egg={egg}
setEgg={setEgg}
/>
</div>
<div css={tw`flex`}>
<ServerImageContainer/>
</div>
</div>
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}>
{egg.relations.variables?.map((v, i) => (
<ServerVariableContainer
key={i}
variable={v}
defaultValue={server.relations?.variables.find(v2 => v.eggId === v2.eggId && v.envVariable === v2.envVariable)?.serverValue || ''}
/>
))}
</div>
<div css={tw`bg-neutral-700 rounded shadow-md py-2 pr-6 mt-6`}>
<div css={tw`flex flex-row`}>
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
Save Changes
</Button>
</div>
</div>
</div>
</Form>
);
}
export default function ServerStartupContainer ({ server }: { server: Server }) {
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const [ egg, setEgg ] = useState<Egg | null>(null);
const setServer = Context.useStoreActions(actions => actions.setServer);
useEffect(() => {
getEgg(server.eggId, [ 'variables' ])
.then(egg => setEgg(egg))
@ -146,61 +201,35 @@ export default function ServerStartupContainer ({ server }: { server: Server })
return (<></>);
}
const submit = () => {
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('server');
updateServerStartup(server.id, values, ServerIncludes)
.then(s => {
setServer({ ...server, ...s });
})
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'server', error });
})
.then(() => setSubmitting(false));
};
return (
<Formik
onSubmit={submit}
initialValues={{
startupCommand: server.container.startupCommand,
startup: server.container.startup,
// Don't ask.
environment: Object.fromEntries(egg.relations.variables?.map(v => [ v.envVariable, '' ]) || []),
image: server.container.image,
eggId: 0,
eggId: server.eggId,
skipScripts: false,
}}
validationSchema={object().shape({})}
validationSchema={object().shape({
})}
>
{({ isSubmitting, isValid }) => (
<div css={tw`flex flex-col`}>
<div css={tw`flex flex-row mb-6`}>
<ServerStartupLineContainer egg={egg}/>
</div>
<div css={tw`grid grid-cols-1 md:grid-cols-2 md:gap-x-8 gap-y-6 md:gap-y-0 mb-6`}>
<div css={tw`flex`}>
<ServerServiceContainer
server={server}
egg={egg}
setEgg={setEgg}
/>
</div>
<div css={tw`flex`}>
<ServerImageContainer/>
</div>
</div>
{egg !== null &&
<div css={tw`grid gap-y-6 gap-x-8 grid-cols-1 md:grid-cols-2`}>
{egg.relations.variables?.map((v, i) => (
<ServerVariableContainer
key={i}
variable={v}
defaultValue={server.relations?.variables.find(v2 => v.eggId === v2.eggId && v.envVariable === v2.envVariable)?.serverValue || ''}
/>
))}
</div>
}
<div css={tw`bg-neutral-700 rounded shadow-md py-2 pr-6 mt-6`}>
<div css={tw`flex flex-row`}>
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
Save Changes
</Button>
</div>
</div>
</div>
)}
<ServerStartupForm server={server}/>
</Formik>
);
}

View file

@ -15,7 +15,7 @@ const FormikSwitch = ({ name, label, ...props }: SwitchProps) => {
form.setFieldTouched(name);
form.setFieldValue(field.name, !field.value);
}}
defaultChecked={field.value}
defaultChecked={ field.value}
{...props}
/>
)}

View file

@ -165,7 +165,7 @@ Route::group(['prefix' => '/servers'], function () {
Route::get('/external/{external_id}', [\Pterodactyl\Http\Controllers\Api\Application\Servers\ExternalServerController::class, 'index']);
Route::patch('/{server}', [\Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController::class, 'update']);
Route::patch('/{server}/build', [\Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController::class, 'build']);
// Route::patch('/{server}/build', [\Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController::class, 'build']);
Route::patch('/{server}/startup', [\Pterodactyl\Http\Controllers\Api\Application\Servers\StartupController::class, 'index']);
Route::post('/', [\Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController::class, 'store']);