forked from Alex/Pterodactyl-Panel
ui(admin): fix server startup variables
This commit is contained in:
parent
cf1cc97340
commit
5e99bb8dd6
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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'),
|
||||
|
@ -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<WithRelationships<No
|
||||
|
||||
return withRelationships(AdminTransformers.toNode(data.data), 'location');
|
||||
};
|
||||
|
||||
export const searchNodes = async (params: QueryBuilderParams<'name'>): Promise<Node[]> => {
|
||||
const { data } = await http.get('/api/application/nodes', {
|
||||
params: withQueryBuilderParams(params),
|
||||
});
|
||||
|
||||
return data.data.map(AdminTransformers.toNode);
|
||||
};
|
||||
|
@ -79,11 +79,11 @@ type LoadedServer = WithRelationships<Server, 'allocations' | 'user' | 'node'>;
|
||||
export const getServer = async (id: number | string): Promise<LoadedServer> => {
|
||||
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');
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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<string, any>;
|
||||
eggId: number;
|
||||
image: string;
|
||||
skipScripts: boolean;
|
||||
startOnCompletion: boolean;
|
||||
}
|
||||
|
||||
export default (r: CreateServerRequest, include: string[] = []): Promise<Server> => {
|
||||
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<Server>
|
||||
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);
|
||||
|
@ -43,7 +43,7 @@ export default (id: number, server: Partial<Values>, 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: {
|
||||
|
@ -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<Record<string, string | undefined>>('environment');
|
||||
const [ eggs, setEggs ] = useState<WithRelationships<Egg, 'variables'>[] | 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<string, any> = {};
|
||||
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<HTMLSelectElement>) => {
|
||||
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 (
|
||||
|
@ -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<CreateServerRequest>();
|
||||
|
||||
export default () => {
|
||||
const [ egg, setEgg ] = useState<Egg | null>(null);
|
||||
|
||||
const submit = (_: Values) => {
|
||||
//
|
||||
return (
|
||||
<Form>
|
||||
<div css={tw`grid grid-cols-2 gap-y-6 gap-x-8 mb-16`}>
|
||||
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}>
|
||||
<BaseSettingsBox>
|
||||
<NodeSelect/>
|
||||
<div css={tw`xl:col-span-2 bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
||||
<FormikSwitch
|
||||
name={'startOnCompletion'}
|
||||
label={'Start after installation'}
|
||||
description={'Should the server be automatically started after it has been installed?'}
|
||||
/>
|
||||
</div>
|
||||
</BaseSettingsBox>
|
||||
<FeatureLimitsBox/>
|
||||
<ServerServiceContainer
|
||||
egg={egg}
|
||||
setEgg={setEgg}
|
||||
/* TODO: Get lowest nest_id rather than always defaulting to 1 */
|
||||
nestId={1}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}>
|
||||
<AdminBox icon={faNetworkWired} title={'Networking'} isLoading={isSubmitting}>
|
||||
<div css={tw`grid grid-cols-1 gap-4 lg:gap-6`}>
|
||||
<div>
|
||||
<Label htmlFor={'allocationId'}>Primary Allocation</Label>
|
||||
<Select id={'allocationId'} name={'allocationId'} disabled>
|
||||
<option value="">Select a node...</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor={'additionalAllocations'}>Additional Allocations</Label>
|
||||
<Select id={'additionalAllocations'} name={'additionalAllocations'} disabled>
|
||||
<option value="">Select a node...</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</AdminBox>
|
||||
<ServerResourceBox/>
|
||||
<ServerImageContainer/>
|
||||
</div>
|
||||
|
||||
<AdminBox title={'Startup Command'} css={tw`relative w-full col-span-2`}>
|
||||
<SpinnerOverlay visible={isSubmitting}/>
|
||||
|
||||
<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}}.'}
|
||||
placeholder={egg?.startup || ''}
|
||||
/>
|
||||
</AdminBox>
|
||||
|
||||
<div css={tw`col-span-2 grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}>
|
||||
{/* 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) => (
|
||||
<ServerVariableContainer
|
||||
key={i}
|
||||
variable={v}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div css={tw`bg-neutral-700 rounded shadow-md px-4 py-3 col-span-2`}>
|
||||
<div css={tw`flex flex-row`}>
|
||||
<Button
|
||||
type="submit"
|
||||
size="small"
|
||||
css={tw`ml-auto`}
|
||||
disabled={isSubmitting || !isValid}
|
||||
>
|
||||
Create Server
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const submit = (r: CreateServerRequest, { setSubmitting }: FormikHelpers<CreateServerRequest>) => {
|
||||
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 }) => (
|
||||
<Form>
|
||||
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8 mb-16`}>
|
||||
<div css={tw`grid grid-cols-1 gap-y-6`}>
|
||||
<BaseSettingsBox/>
|
||||
<FeatureLimitsBox/>
|
||||
{/* TODO: in networking box only show primary allocation and additional allocations */}
|
||||
{/* TODO: add node select */}
|
||||
<ServerServiceContainer
|
||||
egg={egg}
|
||||
setEgg={setEgg}
|
||||
/* TODO: Get lowest nest_id rather than always defaulting to 1 */
|
||||
nestId={1}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`grid grid-cols-1 gap-y-6`}>
|
||||
<AdminBox icon={faNetworkWired} title={'Networking'} isLoading={isSubmitting}>
|
||||
<div css={tw`grid grid-cols-1 gap-4 lg:gap-6`}>
|
||||
<div>
|
||||
<Label htmlFor={'allocationId'}>Primary Allocation</Label>
|
||||
<Select id={'allocationId'} name={'allocationId'}/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor={'additionalAllocations'}>Additional Allocations</Label>
|
||||
<Select id={'additionalAllocations'} name={'additionalAllocations'}/>
|
||||
</div>
|
||||
</div>
|
||||
</AdminBox>
|
||||
<ServerResourceBox/>
|
||||
<ServerImageContainer/>
|
||||
</div>
|
||||
|
||||
<AdminBox title={'Startup Command'} css={tw`relative w-full col-span-2`}>
|
||||
<SpinnerOverlay visible={isSubmitting}/>
|
||||
|
||||
<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}}.'}
|
||||
placeholder={egg?.startup || ''}
|
||||
/>
|
||||
</AdminBox>
|
||||
|
||||
<div css={tw`col-span-2 grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}>
|
||||
{egg?.relationships.variables?.map((v, i) => (
|
||||
<ServerVariableContainer
|
||||
key={i}
|
||||
variable={v}
|
||||
defaultValue={v.defaultValue}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div css={tw`bg-neutral-700 rounded shadow-md px-4 py-3 col-span-2`}>
|
||||
<div css={tw`flex flex-row`}>
|
||||
<Button
|
||||
type="submit"
|
||||
size="small"
|
||||
css={tw`ml-auto`}
|
||||
disabled={isSubmitting || !isValid}
|
||||
>
|
||||
Create Server
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
<InternalForm/>
|
||||
</Formik>
|
||||
</AdminContentBlock>
|
||||
);
|
||||
|
47
resources/scripts/components/admin/servers/NodeSelect.tsx
Normal file
47
resources/scripts/components/admin/servers/NodeSelect.tsx
Normal file
@ -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<Node | null>(selected || null);
|
||||
const [ nodes, setNodes ] = useState<Node[] | null>(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 (
|
||||
<SearchableSelect
|
||||
id={'nodeId'}
|
||||
name={'nodeId'}
|
||||
label={'Node'}
|
||||
placeholder={'Select a node...'}
|
||||
items={nodes}
|
||||
selected={node}
|
||||
setSelected={setNode}
|
||||
setItems={setNodes}
|
||||
onSearch={onSearch}
|
||||
onSelect={onSelect}
|
||||
getSelectedText={getSelectedText}
|
||||
nullable
|
||||
>
|
||||
{nodes?.map(d => (
|
||||
<Option key={d.id} selectId={'nodeId'} id={d.id} item={d} active={d.id === node?.id}>
|
||||
{d.name}
|
||||
</Option>
|
||||
))}
|
||||
</SearchableSelect>
|
||||
);
|
||||
};
|
@ -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<number>(_nestId);
|
||||
|
||||
return (
|
||||
<AdminBox title={'Service Configuration'} isLoading={isSubmitting} css={tw`w-full`}>
|
||||
@ -71,7 +71,7 @@ export function ServerServiceContainer ({ egg, setEgg, nestId: _nestId }: { egg:
|
||||
<EggSelect nestId={nestId} selectedEggId={egg?.id} onEggSelect={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={'Soon™'}/>
|
||||
<FormikSwitch name={'skipScripts'} label={'Skip Egg Install Script'} description={'Soon™'}/>
|
||||
</div>
|
||||
</AdminBox>
|
||||
);
|
||||
@ -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<string | undefined>(key);
|
||||
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(key, defaultValue);
|
||||
}, [ variable, defaultValue ]);
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(value);
|
||||
setTouched(true);
|
||||
}, [ value ]);
|
||||
|
||||
return (
|
||||
<AdminBox css={tw`relative w-full`} title={<p css={tw`text-sm uppercase`}>{variable.name}</p>}>
|
||||
@ -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<Values>();
|
||||
|
||||
return (
|
||||
<Form>
|
||||
@ -150,11 +157,12 @@ function ServerStartupForm ({ egg, setEgg, server }: { egg: Egg | null, setEgg:
|
||||
</div>
|
||||
|
||||
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}>
|
||||
{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) => (
|
||||
<ServerVariableContainer
|
||||
key={i}
|
||||
variable={v}
|
||||
defaultValue={server.relationships?.variables?.find(v2 => 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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -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<string, any>,
|
||||
image: server.container.image,
|
||||
eggId: server.eggId,
|
||||
skipScripts: false,
|
||||
}}
|
||||
validationSchema={object().shape({
|
||||
})}
|
||||
validationSchema={object().shape({})}
|
||||
>
|
||||
<ServerStartupForm
|
||||
egg={egg}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { useFormikContext } from 'formik';
|
||||
import AdminBox from '@/components/admin/AdminBox';
|
||||
@ -7,16 +7,17 @@ import Field from '@/components/elements/Field';
|
||||
import OwnerSelect from '@/components/admin/servers/OwnerSelect';
|
||||
import { useServerFromRoute } from '@/api/admin/server';
|
||||
|
||||
export default () => {
|
||||
export default ({ children }: { children?: ReactNode }) => {
|
||||
const { data: server } = useServerFromRoute();
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
return (
|
||||
<AdminBox icon={faCogs} title={'Settings'} isLoading={isSubmitting}>
|
||||
<div css={tw`grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6`}>
|
||||
<Field id={'name'} name={'name'} label={'Server Name'} type={'text'}/>
|
||||
<Field id={'name'} name={'name'} label={'Server Name'} type={'text'} placeholder={'My Amazing Server'}/>
|
||||
<Field id={'externalId'} name={'externalId'} label={'External Identifier'} type={'text'}/>
|
||||
<OwnerSelect selected={server?.relationships.user}/>
|
||||
{children}
|
||||
</div>
|
||||
</AdminBox>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user