ui(admin): add egg install editing

This commit is contained in:
Matthew Penner 2021-09-17 14:33:38 -06:00
parent e8ddadc608
commit 8d0dd42475
No known key found for this signature in database
GPG key ID: 030E4AB751DC756F
8 changed files with 193 additions and 111 deletions

View file

@ -7,7 +7,6 @@ use Pterodactyl\Models\Nest;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Transformers\Api\Application\EggTransformer; use Pterodactyl\Transformers\Api\Application\EggTransformer;
use Pterodactyl\Http\Requests\Api\Application\Eggs\GetEggRequest; use Pterodactyl\Http\Requests\Api\Application\Eggs\GetEggRequest;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException; use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
@ -19,22 +18,8 @@ use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
class EggController extends ApplicationApiController class EggController extends ApplicationApiController
{ {
private EggRepositoryInterface $repository;
/**
* EggController constructor.
*/
public function __construct(EggRepositoryInterface $repository)
{
parent::__construct();
$this->repository = $repository;
}
/** /**
* Return an array of all eggs on a given nest. * Return an array of all eggs on a given nest.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/ */
public function index(GetEggsRequest $request, Nest $nest): array public function index(GetEggsRequest $request, Nest $nest): array
{ {
@ -58,8 +43,6 @@ class EggController extends ApplicationApiController
/** /**
* Returns a single egg. * Returns a single egg.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/ */
public function view(GetEggRequest $request, Egg $egg): array public function view(GetEggRequest $request, Egg $egg): array
{ {
@ -70,8 +53,6 @@ class EggController extends ApplicationApiController
/** /**
* Creates a new egg. * Creates a new egg.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/ */
public function store(StoreEggRequest $request): JsonResponse public function store(StoreEggRequest $request): JsonResponse
{ {
@ -84,8 +65,6 @@ class EggController extends ApplicationApiController
/** /**
* Updates an egg. * Updates an egg.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/ */
public function update(UpdateEggRequest $request, Egg $egg): array public function update(UpdateEggRequest $request, Egg $egg): array
{ {

View file

@ -2,12 +2,27 @@
namespace Pterodactyl\Http\Requests\Api\Application\Eggs; namespace Pterodactyl\Http\Requests\Api\Application\Eggs;
use Pterodactyl\Models\Egg;
class UpdateEggRequest extends StoreEggRequest class UpdateEggRequest extends StoreEggRequest
{ {
public function rules(array $rules = null): array public function rules(array $rules = null): array
{ {
return $rules ?? Egg::getRulesForUpdate($this->route()->parameter('egg')->id); return [
'nest_id' => 'sometimes|numeric|exists:nests,id',
'name' => 'sometimes|string|max:191',
'description' => 'sometimes|string|nullable',
'features' => 'sometimes|array|nullable',
'docker_images' => 'sometimes|required|array|min:1',
'docker_images.*' => 'sometimes|string',
'file_denylist' => 'sometimes|array|nullable',
'file_denylist.*' => 'sometimes|string',
'config_files' => 'sometimes|nullable|json',
'config_startup' => 'sometimes|nullable|json',
'config_stop' => 'sometimes|nullable|string|max:191',
'config_from' => 'sometimes|nullable|numeric|exists:eggs,id',
'startup' => 'sometimes|nullable|string',
'script_container' => 'sometimes|string',
'script_entry' => 'sometimes|string',
'script_install' => 'sometimes|string',
];
} }
} }

View file

@ -1,12 +1,29 @@
import http from '@/api/http'; import http from '@/api/http';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg'; import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
export default (nestId: number, name: string): Promise<Egg> => { export default (egg: Partial<Egg>): Promise<Egg> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('/api/application/eggs', { http.post(
nestId, name, '/api/application/eggs/',
}) {
.then(({ data }) => resolve(rawDataToEgg(data.attributes))) nest_id: egg.nestId,
name: egg.name,
description: egg.description,
features: egg.features,
docker_images: egg.dockerImages,
config_files: egg.configFiles,
config_startup: egg.configStartup,
config_stop: egg.configStop,
config_from: egg.configFrom,
startup: egg.startup,
script_container: egg.scriptContainer,
copy_script_from: egg.copyScriptFrom,
script_entry: egg.scriptEntry,
script_is_privileged: egg.scriptIsPrivileged,
script_install: egg.scriptInstall,
},
)
.then(({ data }) => resolve(rawDataToEgg(data)))
.catch(reject); .catch(reject);
}); });
}; };

View file

@ -43,7 +43,6 @@ export interface Egg {
dockerImages: string[]; dockerImages: string[];
configFiles: string | null; configFiles: string | null;
configStartup: string | null; configStartup: string | null;
configLogs: string | null;
configStop: string | null; configStop: string | null;
configFrom: number | null; configFrom: number | null;
startup: string; startup: string;
@ -71,11 +70,10 @@ export const rawDataToEgg = ({ attributes }: FractalResponseData): Egg => ({
description: attributes.description, description: attributes.description,
features: attributes.features, features: attributes.features,
dockerImages: attributes.docker_images, dockerImages: attributes.docker_images,
configFiles: attributes.config_files, configFiles: attributes.config?.files,
configStartup: attributes.config_startup, configStartup: attributes.config?.startup,
configLogs: attributes.config_logs, configStop: attributes.config?.stop,
configStop: attributes.config_stop, configFrom: attributes.config?.extends,
configFrom: attributes.config_from,
startup: attributes.startup, startup: attributes.startup,
copyScriptFrom: attributes.copy_script_from, copyScriptFrom: attributes.copy_script_from,
scriptContainer: attributes.script?.container, scriptContainer: attributes.script?.container,

View file

@ -0,0 +1,29 @@
import http from '@/api/http';
import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
export default (id: number, egg: Partial<Egg>): Promise<Egg> => {
return new Promise((resolve, reject) => {
http.patch(
`/api/application/eggs/${id}`,
{
nest_id: egg.nestId,
name: egg.name,
description: egg.description,
features: egg.features,
docker_images: egg.dockerImages,
config_files: egg.configFiles,
config_startup: egg.configStartup,
config_stop: egg.configStop,
config_from: egg.configFrom,
startup: egg.startup,
script_container: egg.scriptContainer,
copy_script_from: egg.copyScriptFrom,
script_entry: egg.scriptEntry,
script_is_privileged: egg.scriptIsPrivileged,
script_install: egg.scriptInstall,
},
)
.then(({ data }) => resolve(rawDataToEgg(data)))
.catch(reject);
});
};

View file

@ -99,50 +99,48 @@ const EditInformationContainer = () => {
description: string().max(255, ''), description: string().max(255, ''),
})} })}
> >
{ {({ isSubmitting, isValid }) => (
({ isSubmitting, isValid }) => ( <React.Fragment>
<React.Fragment> <AdminBox title={'Edit Nest'} css={tw`flex-1 self-start w-full relative mb-8 lg:mb-0 mr-0 lg:mr-4`}>
<AdminBox title={'Edit Nest'} css={tw`flex-1 self-start w-full relative mb-8 lg:mb-0 mr-0 lg:mr-4`}> <SpinnerOverlay visible={isSubmitting}/>
<SpinnerOverlay visible={isSubmitting}/>
<Form css={tw`mb-0`}> <Form css={tw`mb-0`}>
<div> <div>
<Field <Field
id={'name'} id={'name'}
name={'name'} name={'name'}
label={'Name'} label={'Name'}
type={'text'} type={'text'}
/>
</div>
<div css={tw`mt-6`}>
<Field
id={'description'}
name={'description'}
label={'Description'}
type={'text'}
/>
</div>
<div css={tw`w-full flex flex-row items-center mt-6`}>
<div css={tw`flex`}>
<NestDeleteButton
nestId={nest.id}
onDeleted={() => history.push('/admin/nests')}
/> />
</div> </div>
<div css={tw`mt-6`}> <div css={tw`flex ml-auto`}>
<Field <Button type={'submit'} disabled={isSubmitting || !isValid}>
id={'description'} Save
name={'description'} </Button>
label={'Description'}
type={'text'}
/>
</div> </div>
</div>
<div css={tw`w-full flex flex-row items-center mt-6`}> </Form>
<div css={tw`flex`}> </AdminBox>
<NestDeleteButton </React.Fragment>
nestId={nest.id} )}
onDeleted={() => history.push('/admin/nests')}
/>
</div>
<div css={tw`flex ml-auto`}>
<Button type={'submit'} disabled={isSubmitting || !isValid}>
Save
</Button>
</div>
</div>
</Form>
</AdminBox>
</React.Fragment>
)
}
</Formik> </Formik>
); );
}; };

View file

@ -1,52 +1,98 @@
import { Egg } from '@/api/admin/eggs/getEgg';
import updateEgg from '@/api/admin/eggs/updateEgg';
import Field from '@/components/elements/Field';
import useFlash from '@/plugins/useFlash';
import { shell } from '@codemirror/legacy-modes/mode/shell'; import { shell } from '@codemirror/legacy-modes/mode/shell';
import { faScroll } from '@fortawesome/free-solid-svg-icons';
import { Form, Formik, FormikHelpers } from 'formik';
import React from 'react'; import React from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import { Context } from '@/components/admin/nests/eggs/EggRouter';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import Editor from '@/components/elements/Editor'; import Editor from '@/components/elements/Editor';
import Input from '@/components/elements/Input';
import Label from '@/components/elements/Label';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
export default () => { interface Values {
const egg = Context.useStoreState(state => state.egg); scriptContainer: string;
scriptEntry: string;
scriptInstall: string;
}
if (egg === undefined) { export default function EggInstallContainer ({ egg }: { egg: Egg }) {
return ( const { clearFlashes, clearAndAddHttpError } = useFlash();
<></>
); let fetchFileContent: null | (() => Promise<string>) = null;
}
const submit = async (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
if (fetchFileContent === null) {
return;
}
values.scriptInstall = await fetchFileContent();
clearFlashes('egg');
updateEgg(egg.id, values)
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'egg', error });
})
.then(() => setSubmitting(false));
};
return ( return (
<AdminBox title={'Install Script'} padding={false}> <Formik
<div css={tw`relative pb-4`}> onSubmit={submit}
<SpinnerOverlay visible={false}/> initialValues={{
scriptContainer: egg.scriptContainer,
scriptEntry: egg.scriptEntry,
scriptInstall: '',
}}
>
{({ isSubmitting, isValid }) => (
<AdminBox icon={faScroll} title={'Install Script'} padding={false}>
<div css={tw`relative pb-4`}>
<SpinnerOverlay visible={isSubmitting}/>
<Editor overrides={tw`h-96 mb-4`} initialContent={egg.scriptInstall || ''} mode={shell}/> <Form>
<Editor
overrides={tw`h-96 mb-4`}
initialContent={egg.scriptInstall || ''}
mode={shell}
fetchContent={value => {
fetchFileContent = value;
}}
/>
<div css={tw`mx-6 mb-4`}> <div css={tw`mx-6 mb-4`}>
<div css={tw`grid grid-cols-3 gap-x-8 gap-y-6`}> <div css={tw`grid grid-cols-3 gap-x-8 gap-y-6`}>
<div> <Field
<Label>Install Container</Label> id={'scriptContainer'}
<Input type="text" defaultValue={egg.scriptContainer}/> name={'scriptContainer'}
<p className={'input-help'}>The Docker image to use for running this installation script.</p> label={'Install Container'}
</div> type={'text'}
description={'The Docker image to use for running this installation script.'}
/>
<div> <Field
<Label>Install Entrypoint</Label> id={'scriptEntry'}
<Input type="text" defaultValue={egg.scriptEntry}/> name={'scriptEntry'}
<p className={'input-help'}>The command that should be used to run this script inside of the installation container.</p> label={'Install Entrypoint'}
</div> type={'text'}
description={'The command that should be used to run this script inside of the installation container.'}
/>
</div>
</div>
<div css={tw`flex flex-row border-t border-neutral-600`}>
<Button type={'submit'} size={'small'} css={tw`ml-auto mr-6 mt-4`} disabled={isSubmitting || !isValid}>
Save Changes
</Button>
</div>
</Form>
</div> </div>
</div> </AdminBox>
)}
<div css={tw`flex flex-row border-t border-neutral-600`}> </Formik>
<Button type={'button'} size={'small'} css={tw`ml-auto mr-6 mt-4`}>
Save Changes
</Button>
</div>
</div>
</AdminBox>
); );
}; }

View file

@ -101,7 +101,7 @@ const EggRouter = () => {
</Route> </Route>
<Route path={`${match.path}/install`} exact> <Route path={`${match.path}/install`} exact>
<EggInstallContainer/> <EggInstallContainer egg={egg}/>
</Route> </Route>
</Switch> </Switch>
</AdminContentBlock> </AdminContentBlock>