ui(admin): add egg install editing
This commit is contained in:
parent
e8ddadc608
commit
8d0dd42475
8 changed files with 193 additions and 111 deletions
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
29
resources/scripts/api/admin/eggs/updateEgg.ts
Normal file
29
resources/scripts/api/admin/eggs/updateEgg.ts
Normal 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);
|
||||||
|
});
|
||||||
|
};
|
|
@ -99,8 +99,7 @@ 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}/>
|
||||||
|
@ -141,8 +140,7 @@ const EditInformationContainer = () => {
|
||||||
</Form>
|
</Form>
|
||||||
</AdminBox>
|
</AdminBox>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
values.scriptInstall = await fetchFileContent();
|
||||||
<AdminBox title={'Install Script'} padding={false}>
|
|
||||||
<div css={tw`relative pb-4`}>
|
|
||||||
<SpinnerOverlay visible={false}/>
|
|
||||||
|
|
||||||
<Editor overrides={tw`h-96 mb-4`} initialContent={egg.scriptInstall || ''} mode={shell}/>
|
clearFlashes('egg');
|
||||||
|
|
||||||
|
updateEgg(egg.id, values)
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
clearAndAddHttpError({ key: 'egg', error });
|
||||||
|
})
|
||||||
|
.then(() => setSubmitting(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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}/>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<div css={tw`flex flex-row border-t border-neutral-600`}>
|
<div css={tw`flex flex-row border-t border-neutral-600`}>
|
||||||
<Button type={'button'} size={'small'} css={tw`ml-auto mr-6 mt-4`}>
|
<Button type={'submit'} size={'small'} css={tw`ml-auto mr-6 mt-4`} disabled={isSubmitting || !isValid}>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</AdminBox>
|
</AdminBox>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue