ui(admin): fix server startup variables
This commit is contained in:
parent
cf1cc97340
commit
5e99bb8dd6
11 changed files with 292 additions and 237 deletions
|
@ -18,15 +18,9 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||||
'external_id' => $rules['external_id'],
|
'external_id' => $rules['external_id'],
|
||||||
'name' => $rules['name'],
|
'name' => $rules['name'],
|
||||||
'description' => array_merge(['nullable'], $rules['description']),
|
'description' => array_merge(['nullable'], $rules['description']),
|
||||||
'user' => $rules['owner_id'],
|
'owner_id' => $rules['owner_id'],
|
||||||
'egg' => $rules['egg_id'],
|
'node_id' => $rules['node_id'],
|
||||||
'docker_image' => $rules['image'],
|
|
||||||
'startup' => $rules['startup'],
|
|
||||||
'environment' => 'present|array',
|
|
||||||
'skip_scripts' => 'sometimes|boolean',
|
|
||||||
'oom_disabled' => 'sometimes|boolean',
|
|
||||||
|
|
||||||
// Resource limitations
|
|
||||||
'limits' => 'required|array',
|
'limits' => 'required|array',
|
||||||
'limits.memory' => $rules['memory'],
|
'limits.memory' => $rules['memory'],
|
||||||
'limits.swap' => $rules['swap'],
|
'limits.swap' => $rules['swap'],
|
||||||
|
@ -34,26 +28,21 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||||
'limits.io' => $rules['io'],
|
'limits.io' => $rules['io'],
|
||||||
'limits.threads' => $rules['threads'],
|
'limits.threads' => $rules['threads'],
|
||||||
'limits.cpu' => $rules['cpu'],
|
'limits.cpu' => $rules['cpu'],
|
||||||
|
'limits.oom_killer' => 'required|boolean',
|
||||||
|
|
||||||
// Application Resource Limits
|
|
||||||
'feature_limits' => 'required|array',
|
'feature_limits' => 'required|array',
|
||||||
'feature_limits.databases' => $rules['database_limit'],
|
|
||||||
'feature_limits.allocations' => $rules['allocation_limit'],
|
'feature_limits.allocations' => $rules['allocation_limit'],
|
||||||
'feature_limits.backups' => $rules['backup_limit'],
|
'feature_limits.backups' => $rules['backup_limit'],
|
||||||
|
'feature_limits.databases' => $rules['database_limit'],
|
||||||
|
|
||||||
// Placeholders for rules added in withValidator() function.
|
'allocation.default' => 'required|bail|integer|exists:allocations,id',
|
||||||
'allocation.default' => '',
|
'allocation.additional.*' => 'integer|exists:allocations,id',
|
||||||
'allocation.additional.*' => '',
|
|
||||||
|
|
||||||
// Automatic deployment rules
|
'startup' => $rules['startup'],
|
||||||
'deploy' => 'sometimes|required|array',
|
'environment' => 'present|array',
|
||||||
'deploy.locations' => 'array',
|
'egg_id' => $rules['egg_id'],
|
||||||
'deploy.locations.*' => 'integer|min:1',
|
'image' => $rules['image'],
|
||||||
'deploy.dedicated_ip' => 'required_with:deploy,boolean',
|
'skip_scripts' => 'present|boolean',
|
||||||
'deploy.port_range' => 'array',
|
|
||||||
'deploy.port_range.*' => 'string',
|
|
||||||
|
|
||||||
'start_on_completion' => 'sometimes|boolean',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,69 +54,30 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||||
'external_id' => array_get($data, 'external_id'),
|
'external_id' => array_get($data, 'external_id'),
|
||||||
'name' => array_get($data, 'name'),
|
'name' => array_get($data, 'name'),
|
||||||
'description' => array_get($data, 'description'),
|
'description' => array_get($data, 'description'),
|
||||||
'owner_id' => array_get($data, 'user'),
|
'owner_id' => array_get($data, 'owner_id'),
|
||||||
'egg_id' => array_get($data, 'egg'),
|
'node_id' => array_get($data, 'node_id'),
|
||||||
'image' => array_get($data, 'docker_image'),
|
|
||||||
'startup' => array_get($data, 'startup'),
|
|
||||||
'environment' => array_get($data, 'environment'),
|
|
||||||
'memory' => array_get($data, 'limits.memory'),
|
'memory' => array_get($data, 'limits.memory'),
|
||||||
'swap' => array_get($data, 'limits.swap'),
|
'swap' => array_get($data, 'limits.swap'),
|
||||||
'disk' => array_get($data, 'limits.disk'),
|
'disk' => array_get($data, 'limits.disk'),
|
||||||
'io' => array_get($data, 'limits.io'),
|
'io' => array_get($data, 'limits.io'),
|
||||||
'cpu' => array_get($data, 'limits.cpu'),
|
|
||||||
'threads' => array_get($data, 'limits.threads'),
|
'threads' => array_get($data, 'limits.threads'),
|
||||||
'skip_scripts' => array_get($data, 'skip_scripts', false),
|
'cpu' => array_get($data, 'limits.cpu'),
|
||||||
'allocation_id' => array_get($data, 'allocation.default'),
|
'oom_disabled' => !array_get($data, 'limits.oom_killer'),
|
||||||
'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'),
|
|
||||||
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
|
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
|
||||||
'backup_limit' => array_get($data, 'feature_limits.backups'),
|
'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'),
|
'io' => array_get($data, 'limits.io'),
|
||||||
'threads' => array_get($data, 'limits.threads'),
|
'threads' => array_get($data, 'limits.threads'),
|
||||||
'cpu' => array_get($data, 'limits.cpu'),
|
'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'),
|
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
|
||||||
'backup_limit' => array_get($data, 'feature_limits.backups'),
|
'backup_limit' => array_get($data, 'feature_limits.backups'),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Model, UUID, WithRelationships, withRelationships } from '@/api/admin/index';
|
import { Model, UUID, WithRelationships, withRelationships } from '@/api/admin/index';
|
||||||
import { Location } from '@/api/admin/location';
|
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 { AdminTransformers } from '@/api/admin/transformers';
|
||||||
import { Server } from '@/api/admin/server';
|
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');
|
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> => {
|
export const getServer = async (id: number | string): Promise<LoadedServer> => {
|
||||||
const { data } = await http.get(`/api/application/servers/${id}`, {
|
const { data } = await http.get(`/api/application/servers/${id}`, {
|
||||||
params: {
|
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 http from '@/api/http';
|
||||||
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
|
import { Server, rawDataToServer } from '@/api/admin/servers/getServers';
|
||||||
|
|
||||||
interface CreateServerRequest {
|
export interface CreateServerRequest {
|
||||||
|
externalId: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
user: number;
|
ownerId: number;
|
||||||
egg: number;
|
nodeId: number;
|
||||||
dockerImage: string;
|
|
||||||
startup: string;
|
|
||||||
skipScripts: boolean;
|
|
||||||
oomDisabled: boolean;
|
|
||||||
startOnCompletion: boolean;
|
|
||||||
environment: string[];
|
|
||||||
|
|
||||||
allocation: {
|
|
||||||
default: number;
|
|
||||||
additional: number[];
|
|
||||||
};
|
|
||||||
|
|
||||||
limits: {
|
limits: {
|
||||||
cpu: number;
|
|
||||||
disk: number;
|
|
||||||
io: number;
|
|
||||||
memory: number;
|
memory: number;
|
||||||
swap: number;
|
swap: number;
|
||||||
|
disk: number;
|
||||||
|
io: number;
|
||||||
|
cpu: number;
|
||||||
threads: string;
|
threads: string;
|
||||||
};
|
oomDisabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
featureLimits: {
|
featureLimits: {
|
||||||
allocations: number;
|
allocations: number;
|
||||||
backups: number;
|
backups: number;
|
||||||
databases: 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> => {
|
export default (r: CreateServerRequest, include: string[] = []): Promise<Server> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/api/application/servers', {
|
http.post('/api/application/servers', {
|
||||||
|
externalId: r.externalId,
|
||||||
name: r.name,
|
name: r.name,
|
||||||
description: r.description,
|
description: r.description,
|
||||||
user: r.user,
|
owner_id: r.ownerId,
|
||||||
egg: r.egg,
|
node_id: r.nodeId,
|
||||||
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,
|
|
||||||
},
|
|
||||||
|
|
||||||
limits: {
|
limits: {
|
||||||
cpu: r.limits.cpu,
|
cpu: r.limits.cpu,
|
||||||
|
@ -67,6 +60,18 @@ export default (r: CreateServerRequest, include: string[] = []): Promise<Server>
|
||||||
backups: r.featureLimits.backups,
|
backups: r.featureLimits.backups,
|
||||||
databases: r.featureLimits.databases,
|
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(',') } })
|
}, { params: { include: include.join(',') } })
|
||||||
.then(({ data }) => resolve(rawDataToServer(data)))
|
.then(({ data }) => resolve(rawDataToServer(data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default (id: number, server: Partial<Values>, include: string[] = []): Pr
|
||||||
io: server.limits?.io,
|
io: server.limits?.io,
|
||||||
cpu: server.limits?.cpu,
|
cpu: server.limits?.cpu,
|
||||||
threads: server.limits?.threads,
|
threads: server.limits?.threads,
|
||||||
oom_disabled: server.limits?.oomDisabled,
|
oom_killer: server.limits?.oomDisabled,
|
||||||
},
|
},
|
||||||
|
|
||||||
feature_limits: {
|
feature_limits: {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Label from '@/components/elements/Label';
|
|
||||||
import Select from '@/components/elements/Select';
|
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Egg, searchEggs } from '@/api/admin/egg';
|
|
||||||
import { WithRelationships } from '@/api/admin';
|
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 {
|
interface Props {
|
||||||
nestId?: number;
|
nestId?: number;
|
||||||
|
@ -15,31 +15,40 @@ export default ({ nestId, selectedEggId, onEggSelect }: Props) => {
|
||||||
const [ , , { setValue, setTouched } ] = useField<Record<string, string | undefined>>('environment');
|
const [ , , { setValue, setTouched } ] = useField<Record<string, string | undefined>>('environment');
|
||||||
const [ eggs, setEggs ] = useState<WithRelationships<Egg, 'variables'>[] | null>(null);
|
const [ eggs, setEggs ] = useState<WithRelationships<Egg, 'variables'>[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const selectEgg = (egg: Egg | null) => {
|
||||||
if (!nestId) return setEggs(null);
|
if (egg === null) {
|
||||||
|
onEggSelect(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
searchEggs(nestId, {}).then(eggs => {
|
// 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);
|
setEggs(eggs);
|
||||||
onEggSelect(eggs[0] || null);
|
selectEgg(eggs[0] || null);
|
||||||
}).catch(error => console.error(error));
|
})
|
||||||
|
.catch(error => console.error(error));
|
||||||
}, [ nestId ]);
|
}, [ nestId ]);
|
||||||
|
|
||||||
const onSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const onSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
if (!eggs) return;
|
selectEgg(eggs?.find(egg => egg.id.toString() === e.currentTarget.value) || null);
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,77 +1,45 @@
|
||||||
import { Egg } from '@/api/admin/egg';
|
import { Egg } from '@/api/admin/egg';
|
||||||
import AdminBox from '@/components/admin/AdminBox';
|
import AdminBox from '@/components/admin/AdminBox';
|
||||||
|
import NodeSelect from '@/components/admin/servers/NodeSelect';
|
||||||
import { ServerImageContainer, ServerServiceContainer, ServerVariableContainer } from '@/components/admin/servers/ServerStartupContainer';
|
import { ServerImageContainer, ServerServiceContainer, ServerVariableContainer } from '@/components/admin/servers/ServerStartupContainer';
|
||||||
import BaseSettingsBox from '@/components/admin/servers/settings/BaseSettingsBox';
|
import BaseSettingsBox from '@/components/admin/servers/settings/BaseSettingsBox';
|
||||||
import FeatureLimitsBox from '@/components/admin/servers/settings/FeatureLimitsBox';
|
import FeatureLimitsBox from '@/components/admin/servers/settings/FeatureLimitsBox';
|
||||||
import ServerResourceBox from '@/components/admin/servers/settings/ServerResourceBox';
|
import ServerResourceBox from '@/components/admin/servers/settings/ServerResourceBox';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
|
import FormikSwitch from '@/components/elements/FormikSwitch';
|
||||||
import Label from '@/components/elements/Label';
|
import Label from '@/components/elements/Label';
|
||||||
import Select from '@/components/elements/Select';
|
import Select from '@/components/elements/Select';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
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 React, { useState } from 'react';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||||
import { object } from 'yup';
|
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 [ egg, setEgg ] = useState<Egg | null>(null);
|
||||||
|
|
||||||
const submit = (_: Values) => {
|
|
||||||
//
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminContentBlock title={'New Server'}>
|
|
||||||
<div css={tw`w-full flex flex-row items-center mb-8`}>
|
|
||||||
<div css={tw`flex flex-col flex-shrink`} style={{ minWidth: '0' }}>
|
|
||||||
<h2 css={tw`text-2xl text-neutral-50 font-header font-medium`}>New Server</h2>
|
|
||||||
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>Add a new server to the panel.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FlashMessageRender byKey={'server:create'} css={tw`mb-4`}/>
|
|
||||||
|
|
||||||
<Formik
|
|
||||||
onSubmit={submit}
|
|
||||||
initialValues={{
|
|
||||||
externalId: '',
|
|
||||||
name: '',
|
|
||||||
ownerId: 0,
|
|
||||||
limits: {
|
|
||||||
memory: 0,
|
|
||||||
swap: 0,
|
|
||||||
disk: 0,
|
|
||||||
io: 0,
|
|
||||||
cpu: 0,
|
|
||||||
threads: '',
|
|
||||||
// This value is inverted to have the switch be on when the
|
|
||||||
// OOM Killer is enabled, rather than when disabled.
|
|
||||||
oomDisabled: false,
|
|
||||||
},
|
|
||||||
featureLimits: {
|
|
||||||
allocations: 1,
|
|
||||||
backups: 0,
|
|
||||||
databases: 0,
|
|
||||||
},
|
|
||||||
allocationId: 0,
|
|
||||||
addAllocations: [] as number[],
|
|
||||||
removeAllocations: [] as number[],
|
|
||||||
}}
|
|
||||||
validationSchema={object().shape({})}
|
|
||||||
>
|
|
||||||
{({ isSubmitting, isValid }) => (
|
|
||||||
<Form>
|
<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-2 gap-y-6 gap-x-8 mb-16`}>
|
||||||
<div css={tw`grid grid-cols-1 gap-y-6`}>
|
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}>
|
||||||
<BaseSettingsBox/>
|
<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/>
|
<FeatureLimitsBox/>
|
||||||
{/* TODO: in networking box only show primary allocation and additional allocations */}
|
|
||||||
{/* TODO: add node select */}
|
|
||||||
<ServerServiceContainer
|
<ServerServiceContainer
|
||||||
egg={egg}
|
egg={egg}
|
||||||
setEgg={setEgg}
|
setEgg={setEgg}
|
||||||
|
@ -79,16 +47,20 @@ export default () => {
|
||||||
nestId={1}
|
nestId={1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`grid grid-cols-1 gap-y-6`}>
|
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}>
|
||||||
<AdminBox icon={faNetworkWired} title={'Networking'} isLoading={isSubmitting}>
|
<AdminBox icon={faNetworkWired} title={'Networking'} isLoading={isSubmitting}>
|
||||||
<div css={tw`grid grid-cols-1 gap-4 lg:gap-6`}>
|
<div css={tw`grid grid-cols-1 gap-4 lg:gap-6`}>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor={'allocationId'}>Primary Allocation</Label>
|
<Label htmlFor={'allocationId'}>Primary Allocation</Label>
|
||||||
<Select id={'allocationId'} name={'allocationId'}/>
|
<Select id={'allocationId'} name={'allocationId'} disabled>
|
||||||
|
<option value="">Select a node...</option>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor={'additionalAllocations'}>Additional Allocations</Label>
|
<Label htmlFor={'additionalAllocations'}>Additional Allocations</Label>
|
||||||
<Select id={'additionalAllocations'} name={'additionalAllocations'}/>
|
<Select id={'additionalAllocations'} name={'additionalAllocations'} disabled>
|
||||||
|
<option value="">Select a node...</option>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AdminBox>
|
</AdminBox>
|
||||||
|
@ -110,11 +82,11 @@ export default () => {
|
||||||
</AdminBox>
|
</AdminBox>
|
||||||
|
|
||||||
<div css={tw`col-span-2 grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}>
|
<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) => (
|
{/* 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
|
<ServerVariableContainer
|
||||||
key={i}
|
key={i}
|
||||||
variable={v}
|
variable={v}
|
||||||
defaultValue={v.defaultValue}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -133,7 +105,64 @@ export default () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const submit = (r: CreateServerRequest, { setSubmitting }: FormikHelpers<CreateServerRequest>) => {
|
||||||
|
console.log(r);
|
||||||
|
setSubmitting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminContentBlock title={'New Server'}>
|
||||||
|
<div css={tw`w-full flex flex-row items-center mb-8`}>
|
||||||
|
<div css={tw`flex flex-col flex-shrink`} style={{ minWidth: '0' }}>
|
||||||
|
<h2 css={tw`text-2xl text-neutral-50 font-header font-medium`}>New Server</h2>
|
||||||
|
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>Add a new server to the panel.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FlashMessageRender byKey={'server:create'} css={tw`mb-4`}/>
|
||||||
|
|
||||||
|
<Formik
|
||||||
|
onSubmit={submit}
|
||||||
|
initialValues={{
|
||||||
|
externalId: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
ownerId: 0,
|
||||||
|
nodeId: 0,
|
||||||
|
limits: {
|
||||||
|
memory: 1024,
|
||||||
|
swap: 0,
|
||||||
|
disk: 4096,
|
||||||
|
io: 500,
|
||||||
|
cpu: 0,
|
||||||
|
threads: '',
|
||||||
|
// This value is inverted to have the switch be on when the
|
||||||
|
// OOM Killer is enabled, rather than when disabled.
|
||||||
|
oomDisabled: false,
|
||||||
|
},
|
||||||
|
featureLimits: {
|
||||||
|
allocations: 1,
|
||||||
|
backups: 0,
|
||||||
|
databases: 0,
|
||||||
|
},
|
||||||
|
allocation: {
|
||||||
|
default: 0,
|
||||||
|
additional: [] as number[],
|
||||||
|
},
|
||||||
|
startup: '',
|
||||||
|
environment: [],
|
||||||
|
eggId: 0,
|
||||||
|
image: '',
|
||||||
|
skipScripts: false,
|
||||||
|
startOnCompletion: true,
|
||||||
|
} as CreateServerRequest}
|
||||||
|
validationSchema={object().shape({})}
|
||||||
|
>
|
||||||
|
<InternalForm/>
|
||||||
</Formik>
|
</Formik>
|
||||||
</AdminContentBlock>
|
</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 tw from 'twin.macro';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
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 { ApplicationStore } from '@/state';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
import { Actions, useStoreActions } from 'easy-peasy';
|
||||||
import Label from '@/components/elements/Label';
|
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 }) {
|
export function ServerServiceContainer ({ egg, setEgg, nestId: _nestId }: { egg: Egg | null, setEgg: (value: Egg | null) => void, nestId: number }) {
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
const [ nestId, setNestId ] = useState(_nestId);
|
const [ nestId, setNestId ] = useState<number>(_nestId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminBox title={'Service Configuration'} isLoading={isSubmitting} css={tw`w-full`}>
|
<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}/>
|
<EggSelect nestId={nestId} selectedEggId={egg?.id} onEggSelect={setEgg}/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
<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>
|
</div>
|
||||||
</AdminBox>
|
</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 key = 'environment.' + variable.environmentVariable;
|
||||||
|
|
||||||
const { isSubmitting, setFieldValue } = useFormikContext();
|
const [ , , { setValue, setTouched } ] = useField<string | undefined>(key);
|
||||||
|
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFieldValue(key, defaultValue);
|
if (value === undefined) {
|
||||||
}, [ variable, defaultValue ]);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value);
|
||||||
|
setTouched(true);
|
||||||
|
}, [ value ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminBox css={tw`relative w-full`} title={<p css={tw`text-sm uppercase`}>{variable.name}</p>}>
|
<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 }) {
|
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 (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
|
@ -150,11 +157,12 @@ function ServerStartupForm ({ egg, setEgg, server }: { egg: Egg | null, setEgg:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}>
|
<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
|
<ServerVariableContainer
|
||||||
key={i}
|
key={i}
|
||||||
variable={v}
|
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>
|
</div>
|
||||||
|
@ -205,14 +213,12 @@ export default () => {
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
startup: server.container.startup,
|
startup: server.container.startup,
|
||||||
// Don't ask.
|
environment: [] as Record<string, any>,
|
||||||
environment: Object.fromEntries(egg?.relationships.variables.map(v => [ v.environmentVariable, '' ]) || []),
|
|
||||||
image: server.container.image,
|
image: server.container.image,
|
||||||
eggId: server.eggId,
|
eggId: server.eggId,
|
||||||
skipScripts: false,
|
skipScripts: false,
|
||||||
}}
|
}}
|
||||||
validationSchema={object().shape({
|
validationSchema={object().shape({})}
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<ServerStartupForm
|
<ServerStartupForm
|
||||||
egg={egg}
|
egg={egg}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import AdminBox from '@/components/admin/AdminBox';
|
import AdminBox from '@/components/admin/AdminBox';
|
||||||
|
@ -7,16 +7,17 @@ import Field from '@/components/elements/Field';
|
||||||
import OwnerSelect from '@/components/admin/servers/OwnerSelect';
|
import OwnerSelect from '@/components/admin/servers/OwnerSelect';
|
||||||
import { useServerFromRoute } from '@/api/admin/server';
|
import { useServerFromRoute } from '@/api/admin/server';
|
||||||
|
|
||||||
export default () => {
|
export default ({ children }: { children?: ReactNode }) => {
|
||||||
const { data: server } = useServerFromRoute();
|
const { data: server } = useServerFromRoute();
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminBox icon={faCogs} title={'Settings'} isLoading={isSubmitting}>
|
<AdminBox icon={faCogs} title={'Settings'} isLoading={isSubmitting}>
|
||||||
<div css={tw`grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6`}>
|
<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'}/>
|
<Field id={'externalId'} name={'externalId'} label={'External Identifier'} type={'text'}/>
|
||||||
<OwnerSelect selected={server?.relationships.user}/>
|
<OwnerSelect selected={server?.relationships.user}/>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</AdminBox>
|
</AdminBox>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue