ui(admin): basic server creation
This commit is contained in:
parent
cc2ed97b0f
commit
70cf5c17aa
8 changed files with 81 additions and 29 deletions
|
@ -79,7 +79,7 @@ class ServerController extends ApplicationApiController
|
||||||
*/
|
*/
|
||||||
public function store(StoreServerRequest $request): JsonResponse
|
public function store(StoreServerRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$server = $this->creationService->handle($request->validated(), $request->getDeploymentObject());
|
$server = $this->creationService->handle($request->validated());
|
||||||
|
|
||||||
return $this->fractal->item($server)
|
return $this->fractal->item($server)
|
||||||
->transformWith(ServerTransformer::class)
|
->transformWith(ServerTransformer::class)
|
||||||
|
|
|
@ -74,7 +74,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||||
|
|
||||||
'startup' => array_get($data, 'startup'),
|
'startup' => array_get($data, 'startup'),
|
||||||
'environment' => array_get($data, 'environment'),
|
'environment' => array_get($data, 'environment'),
|
||||||
'egg_id' => array_get($data, 'egg'),
|
'egg_id' => array_get($data, 'egg_id'),
|
||||||
'image' => array_get($data, 'image'),
|
'image' => array_get($data, 'image'),
|
||||||
'skip_scripts' => array_get($data, 'skip_scripts'),
|
'skip_scripts' => array_get($data, 'skip_scripts'),
|
||||||
'start_on_completion' => array_get($data, 'start_on_completion', false),
|
'start_on_completion' => array_get($data, 'start_on_completion', false),
|
||||||
|
|
|
@ -74,3 +74,11 @@ export const searchNodes = async (params: QueryBuilderParams<'name'>): Promise<N
|
||||||
|
|
||||||
return data.data.map(AdminTransformers.toNode);
|
return data.data.map(AdminTransformers.toNode);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAllocations = async (id: string | number, params?: QueryBuilderParams<'ip' | 'server_id'>): Promise<Allocation[]> => {
|
||||||
|
const { data } = await http.get(`/api/application/nodes/${id}/allocations`, {
|
||||||
|
params: withQueryBuilderParams(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.data.map(AdminTransformers.toAllocation);
|
||||||
|
};
|
||||||
|
|
|
@ -53,9 +53,10 @@ export default (r: CreateServerRequest, include: string[] = []): Promise<Server>
|
||||||
memory: r.limits.memory,
|
memory: r.limits.memory,
|
||||||
swap: r.limits.swap,
|
swap: r.limits.swap,
|
||||||
threads: r.limits.threads,
|
threads: r.limits.threads,
|
||||||
|
oom_killer: r.limits.oomDisabled,
|
||||||
},
|
},
|
||||||
|
|
||||||
featureLimits: {
|
feature_limits: {
|
||||||
allocations: r.featureLimits.allocations,
|
allocations: r.featureLimits.allocations,
|
||||||
backups: r.featureLimits.backups,
|
backups: r.featureLimits.backups,
|
||||||
databases: r.featureLimits.databases,
|
databases: r.featureLimits.databases,
|
||||||
|
|
|
@ -13,7 +13,12 @@ export default ({ selectedNestId, onNestSelect }: Props) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
searchNests({})
|
searchNests({})
|
||||||
.then(setNests)
|
.then(nests => {
|
||||||
|
setNests(nests);
|
||||||
|
if (selectedNestId === 0 && nests.length > 0) {
|
||||||
|
onNestSelect(nests[0].id);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(error => console.error(error));
|
.catch(error => console.error(error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -12,25 +12,50 @@ 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 useFlash from '@/plugins/useFlash';
|
||||||
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
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 { CreateServerRequest } from '@/api/admin/servers/createServer';
|
import createServer, { CreateServerRequest } from '@/api/admin/servers/createServer';
|
||||||
|
import { Allocation, Node, getAllocations } from '@/api/admin/node';
|
||||||
|
|
||||||
function InternalForm () {
|
function InternalForm () {
|
||||||
const { isSubmitting, isValid, values: { environment } } = useFormikContext<CreateServerRequest>();
|
const { isSubmitting, isValid, setFieldValue, values: { environment } } = useFormikContext<CreateServerRequest>();
|
||||||
|
|
||||||
const [ egg, setEgg ] = useState<Egg | null>(null);
|
const [ egg, setEgg ] = useState<Egg | null>(null);
|
||||||
|
const [ node, setNode ] = useState<Node | null>(null);
|
||||||
|
const [ allocations, setAllocations ] = useState<Allocation[] | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (egg === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFieldValue('eggId', egg.id);
|
||||||
|
setFieldValue('startup', egg.startup);
|
||||||
|
setFieldValue('image', egg.dockerImages.length > 0 ? egg.dockerImages[0] : '');
|
||||||
|
}, [ egg ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (node === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// server_id: 0 filters out assigned allocations
|
||||||
|
getAllocations(node.id, { filters: { server_id: '0' } })
|
||||||
|
.then(setAllocations);
|
||||||
|
}, [ node ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<div css={tw`grid 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 col-span-2 md:col-span-1`}>
|
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}>
|
||||||
<BaseSettingsBox>
|
<BaseSettingsBox>
|
||||||
<NodeSelect/>
|
<NodeSelect node={node} setNode={setNode}/>
|
||||||
<div css={tw`xl:col-span-2 bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
<div css={tw`xl:col-span-2 bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
||||||
<FormikSwitch
|
<FormikSwitch
|
||||||
name={'startOnCompletion'}
|
name={'startOnCompletion'}
|
||||||
|
@ -43,25 +68,32 @@ function InternalForm () {
|
||||||
<ServerServiceContainer
|
<ServerServiceContainer
|
||||||
egg={egg}
|
egg={egg}
|
||||||
setEgg={setEgg}
|
setEgg={setEgg}
|
||||||
/* TODO: Get lowest nest_id rather than always defaulting to 1 */
|
nestId={0}
|
||||||
nestId={1}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}>
|
<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={'allocation.default'}>Primary Allocation</Label>
|
||||||
<Select id={'allocationId'} name={'allocationId'} disabled>
|
<Select
|
||||||
<option value="">Select a node...</option>
|
id={'allocation.default'}
|
||||||
</Select>
|
name={'allocation.default'}
|
||||||
</div>
|
disabled={node === null}
|
||||||
<div>
|
onChange={e => setFieldValue('allocation.default', Number(e.currentTarget.value))}
|
||||||
<Label htmlFor={'additionalAllocations'}>Additional Allocations</Label>
|
>
|
||||||
<Select id={'additionalAllocations'} name={'additionalAllocations'} disabled>
|
{node === null ? <option value="">Select a node...</option> : <option value="">Select an allocation...</option>}
|
||||||
<option value="">Select a node...</option>
|
{allocations?.map(a => <option key={a.id} value={a.id.toString()}>{a.getDisplayText()}</option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
{/*<div>*/}
|
||||||
|
{/* /!* TODO: Multi-select *!/*/}
|
||||||
|
{/* <Label htmlFor={'allocation.additional'}>Additional Allocations</Label>*/}
|
||||||
|
{/* <Select id={'allocation.additional'} name={'allocation.additional'} disabled={node === null}>*/}
|
||||||
|
{/* {node === null ? <option value="">Select a node...</option> : <option value="">Select additional allocations...</option>}*/}
|
||||||
|
{/* {allocations?.map(a => <option key={a.id} value={a.id.toString()}>{a.getDisplayText()}</option>)}*/}
|
||||||
|
{/* </Select>*/}
|
||||||
|
{/*</div>*/}
|
||||||
</div>
|
</div>
|
||||||
</AdminBox>
|
</AdminBox>
|
||||||
<ServerResourceBox/>
|
<ServerResourceBox/>
|
||||||
|
@ -109,9 +141,18 @@ function InternalForm () {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
|
|
||||||
const submit = (r: CreateServerRequest, { setSubmitting }: FormikHelpers<CreateServerRequest>) => {
|
const submit = (r: CreateServerRequest, { setSubmitting }: FormikHelpers<CreateServerRequest>) => {
|
||||||
console.log(r);
|
console.log(r);
|
||||||
setSubmitting(false);
|
clearFlashes('server:create');
|
||||||
|
|
||||||
|
createServer(r)
|
||||||
|
.then(s => history.push(`/admin/servers/${s.id}`))
|
||||||
|
.catch(error => clearAndAddHttpError({ key: 'server:create', error }))
|
||||||
|
.then(() => setSubmitting(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -3,21 +3,18 @@ import { useFormikContext } from 'formik';
|
||||||
import SearchableSelect, { Option } from '@/components/elements/SearchableSelect';
|
import SearchableSelect, { Option } from '@/components/elements/SearchableSelect';
|
||||||
import { Node, searchNodes } from '@/api/admin/node';
|
import { Node, searchNodes } from '@/api/admin/node';
|
||||||
|
|
||||||
export default ({ selected }: { selected?: Node }) => {
|
export default ({ node, setNode }: { node: Node | null, setNode: (_: Node | null) => void }) => {
|
||||||
const context = useFormikContext();
|
const { setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
const [ node, setNode ] = useState<Node | null>(selected || null);
|
|
||||||
const [ nodes, setNodes ] = useState<Node[] | null>(null);
|
const [ nodes, setNodes ] = useState<Node[] | null>(null);
|
||||||
|
|
||||||
const onSearch = async (query: string) => {
|
const onSearch = async (query: string) => {
|
||||||
setNodes(
|
setNodes(await searchNodes({ filters: { name: query } }));
|
||||||
await searchNodes({ filters: { name: query } }),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (node: Node | null) => {
|
const onSelect = (node: Node | null) => {
|
||||||
setNode(node);
|
setNode(node);
|
||||||
context.setFieldValue('ownerId', node?.id || null);
|
setFieldValue('nodeId', node?.id || null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedText = (node: Node | null): string => node?.name || '';
|
const getSelectedText = (node: Node | null): string => node?.name || '';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import SearchableSelect, { Option } from '@/components/elements/SearchableSelect
|
||||||
import { User, searchUserAccounts } from '@/api/admin/user';
|
import { User, searchUserAccounts } from '@/api/admin/user';
|
||||||
|
|
||||||
export default ({ selected }: { selected?: User }) => {
|
export default ({ selected }: { selected?: User }) => {
|
||||||
const context = useFormikContext();
|
const { setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
const [ user, setUser ] = useState<User | null>(selected || null);
|
const [ user, setUser ] = useState<User | null>(selected || null);
|
||||||
const [ users, setUsers ] = useState<User[] | null>(null);
|
const [ users, setUsers ] = useState<User[] | null>(null);
|
||||||
|
@ -17,7 +17,7 @@ export default ({ selected }: { selected?: User }) => {
|
||||||
|
|
||||||
const onSelect = (user: User | null) => {
|
const onSelect = (user: User | null) => {
|
||||||
setUser(user);
|
setUser(user);
|
||||||
context.setFieldValue('ownerId', user?.id || null);
|
setFieldValue('ownerId', user?.id || null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedText = (user: User | null): string => user?.email || '';
|
const getSelectedText = (user: User | null): string => user?.email || '';
|
||||||
|
|
Loading…
Reference in a new issue