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\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Transformers\Api\Application\EggTransformer;
use Pterodactyl\Http\Requests\Api\Application\Eggs\GetEggRequest;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
@ -19,22 +18,8 @@ use Pterodactyl\Http\Controllers\Api\Application\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.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function index(GetEggsRequest $request, Nest $nest): array
{
@ -58,8 +43,6 @@ class EggController extends ApplicationApiController
/**
* Returns a single egg.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function view(GetEggRequest $request, Egg $egg): array
{
@ -70,8 +53,6 @@ class EggController extends ApplicationApiController
/**
* Creates a new egg.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function store(StoreEggRequest $request): JsonResponse
{
@ -84,8 +65,6 @@ class EggController extends ApplicationApiController
/**
* Updates an egg.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function update(UpdateEggRequest $request, Egg $egg): array
{

View file

@ -2,12 +2,27 @@
namespace Pterodactyl\Http\Requests\Api\Application\Eggs;
use Pterodactyl\Models\Egg;
class UpdateEggRequest extends StoreEggRequest
{
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 { 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) => {
http.post('/api/application/eggs', {
nestId, name,
})
.then(({ data }) => resolve(rawDataToEgg(data.attributes)))
http.post(
'/api/application/eggs/',
{
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

@ -43,7 +43,6 @@ export interface Egg {
dockerImages: string[];
configFiles: string | null;
configStartup: string | null;
configLogs: string | null;
configStop: string | null;
configFrom: number | null;
startup: string;
@ -71,11 +70,10 @@ export const rawDataToEgg = ({ attributes }: FractalResponseData): Egg => ({
description: attributes.description,
features: attributes.features,
dockerImages: attributes.docker_images,
configFiles: attributes.config_files,
configStartup: attributes.config_startup,
configLogs: attributes.config_logs,
configStop: attributes.config_stop,
configFrom: attributes.config_from,
configFiles: attributes.config?.files,
configStartup: attributes.config?.startup,
configStop: attributes.config?.stop,
configFrom: attributes.config?.extends,
startup: attributes.startup,
copyScriptFrom: attributes.copy_script_from,
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, ''),
})}
>
{
({ isSubmitting, isValid }) => (
<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`}>
<SpinnerOverlay visible={isSubmitting}/>
{({ isSubmitting, isValid }) => (
<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`}>
<SpinnerOverlay visible={isSubmitting}/>
<Form css={tw`mb-0`}>
<div>
<Field
id={'name'}
name={'name'}
label={'Name'}
type={'text'}
<Form css={tw`mb-0`}>
<div>
<Field
id={'name'}
name={'name'}
label={'Name'}
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 css={tw`mt-6`}>
<Field
id={'description'}
name={'description'}
label={'Description'}
type={'text'}
/>
<div css={tw`flex ml-auto`}>
<Button type={'submit'} disabled={isSubmitting || !isValid}>
Save
</Button>
</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 css={tw`flex ml-auto`}>
<Button type={'submit'} disabled={isSubmitting || !isValid}>
Save
</Button>
</div>
</div>
</Form>
</AdminBox>
</React.Fragment>
)
}
</div>
</Form>
</AdminBox>
</React.Fragment>
)}
</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 { faScroll } from '@fortawesome/free-solid-svg-icons';
import { Form, Formik, FormikHelpers } from 'formik';
import React from 'react';
import tw from 'twin.macro';
import AdminBox from '@/components/admin/AdminBox';
import { Context } from '@/components/admin/nests/eggs/EggRouter';
import Button from '@/components/elements/Button';
import Editor from '@/components/elements/Editor';
import Input from '@/components/elements/Input';
import Label from '@/components/elements/Label';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
export default () => {
const egg = Context.useStoreState(state => state.egg);
interface Values {
scriptContainer: string;
scriptEntry: string;
scriptInstall: string;
}
if (egg === undefined) {
return (
<></>
);
}
export default function EggInstallContainer ({ egg }: { egg: Egg }) {
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 (
<AdminBox title={'Install Script'} padding={false}>
<div css={tw`relative pb-4`}>
<SpinnerOverlay visible={false}/>
<Formik
onSubmit={submit}
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`grid grid-cols-3 gap-x-8 gap-y-6`}>
<div>
<Label>Install Container</Label>
<Input type="text" defaultValue={egg.scriptContainer}/>
<p className={'input-help'}>The Docker image to use for running this installation script.</p>
</div>
<div css={tw`mx-6 mb-4`}>
<div css={tw`grid grid-cols-3 gap-x-8 gap-y-6`}>
<Field
id={'scriptContainer'}
name={'scriptContainer'}
label={'Install Container'}
type={'text'}
description={'The Docker image to use for running this installation script.'}
/>
<div>
<Label>Install Entrypoint</Label>
<Input type="text" defaultValue={egg.scriptEntry}/>
<p className={'input-help'}>The command that should be used to run this script inside of the installation container.</p>
</div>
<Field
id={'scriptEntry'}
name={'scriptEntry'}
label={'Install Entrypoint'}
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 css={tw`flex flex-row border-t border-neutral-600`}>
<Button type={'button'} size={'small'} css={tw`ml-auto mr-6 mt-4`}>
Save Changes
</Button>
</div>
</div>
</AdminBox>
</AdminBox>
)}
</Formik>
);
};
}

View file

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