ui(admin): server edit cleanup, fix startup form
This commit is contained in:
parent
95f3eb54db
commit
df895f4a9f
12 changed files with 422 additions and 322 deletions
|
@ -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'),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
28
resources/scripts/api/admin/servers/updateServerStartup.ts
Normal file
28
resources/scripts/api/admin/servers/updateServerStartup.ts
Normal 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);
|
||||
});
|
||||
};
|
|
@ -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()}>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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[],
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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']);
|
||||
|
|
Loading…
Reference in a new issue