ui(admin): basic server creation

This commit is contained in:
Matthew Penner 2021-10-29 00:04:28 -06:00
parent cc2ed97b0f
commit 70cf5c17aa
No known key found for this signature in database
GPG key ID: BAB67850901908A8
8 changed files with 81 additions and 29 deletions

View file

@ -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)

View file

@ -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),

View file

@ -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);
};

View file

@ -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,

View file

@ -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));
}, []); }, []);

View file

@ -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 (

View file

@ -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 || '';

View file

@ -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 || '';