ui(admin): finish egg variable editing

This commit is contained in:
Matthew Penner 2021-10-03 14:23:06 -06:00
parent 749dc70e71
commit 1eed25dcc7
No known key found for this signature in database
GPG Key ID: BAB67850901908A8
13 changed files with 251 additions and 97 deletions

View File

@ -0,0 +1,65 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Eggs;
use Pterodactyl\Models\Egg;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Eggs\Variables\VariableUpdateService;
use Pterodactyl\Services\Eggs\Variables\VariableCreationService;
use Pterodactyl\Transformers\Api\Application\EggVariableTransformer;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Eggs\Variables\StoreEggVariableRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\Variables\UpdateEggVariablesRequest;
class EggVariableController extends ApplicationApiController
{
private ConnectionInterface $connection;
private VariableCreationService $variableCreationService;
private VariableUpdateService $variableUpdateService;
public function __construct(ConnectionInterface $connection, VariableCreationService $variableCreationService, VariableUpdateService $variableUpdateService)
{
parent::__construct();
$this->connection = $connection;
$this->variableCreationService = $variableCreationService;
$this->variableUpdateService = $variableUpdateService;
}
/**
* Creates a new egg variable.
*
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function store(StoreEggVariableRequest $request, Egg $egg): array
{
$variable = $this->variableCreationService->handle($egg->id, $request->validated());
return $this->fractal->item($variable)
->transformWith(EggVariableTransformer::class)
->toArray();
}
/**
* Updates multiple egg variables.
*
* @throws \Throwable
*/
public function update(UpdateEggVariablesRequest $request, Egg $egg): array
{
$validated = $request->validated();
$this->connection->transaction(function () use($egg, $validated) {
foreach ($validated as $data) {
$this->variableUpdateService->handle($egg, $data);
}
});
return $this->fractal->collection($egg->refresh()->variables)
->transformWith(EggVariableTransformer::class)
->toArray();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Eggs\Variables;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreEggVariableRequest extends ApplicationApiRequest
{
public function rules(array $rules = null): array
{
return [
'name' => 'required|string|min:1|max:191',
'description' => 'sometimes|string|nullable',
'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
'default_value' => 'present',
'user_viewable' => 'required|boolean',
'user_editable' => 'required|boolean',
'rules' => 'bail|required|string',
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Eggs\Variables;
use Pterodactyl\Models\EggVariable;
use Illuminate\Validation\Validator;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class UpdateEggVariablesRequest extends ApplicationApiRequest
{
public function rules(array $rules = null): array
{
return [
'*' => 'array',
'*.id' => 'required|integer',
'*.name' => 'sometimes|string|min:1|max:191',
'*.description' => 'sometimes|string|nullable',
'*.env_variable' => 'sometimes|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
'*.default_value' => 'sometimes|present',
'*.user_viewable' => 'sometimes|boolean',
'*.user_editable' => 'sometimes|boolean',
'*.rules' => 'sometimes|string',
];
}
}

View File

@ -14,7 +14,6 @@ namespace Pterodactyl\Models;
* @property string $rules
* @property \Carbon\CarbonImmutable $created_at
* @property \Carbon\CarbonImmutable $updated_at
* @property bool $required
* @property \Pterodactyl\Models\Egg $egg
* @property \Pterodactyl\Models\ServerVariable $serverVariable
*

View File

@ -3,31 +3,21 @@
namespace Pterodactyl\Services\Eggs\Variables;
use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Contracts\Validation\Factory as Validator;
use Pterodactyl\Traits\Services\ValidatesValidationRules;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException;
class VariableCreationService
{
use ValidatesValidationRules;
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface
*/
private $repository;
/**
* @var \Illuminate\Contracts\Validation\Factory
*/
private $validator;
private Validator $validator;
/**
* VariableCreationService constructor.
*/
public function __construct(EggVariableRepositoryInterface $repository, Factory $validator)
public function __construct(Validator $validator)
{
$this->repository = $repository;
$this->validator = $validator;
}
@ -35,7 +25,7 @@ class VariableCreationService
* Return the validation factory instance to be used by rule validation
* checking in the trait.
*/
protected function getValidator(): Factory
protected function getValidator(): Validator
{
return $this->validator;
}
@ -43,7 +33,6 @@ class VariableCreationService
/**
* Create a new variable for a given Egg.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
@ -59,7 +48,8 @@ class VariableCreationService
$options = array_get($data, 'options') ?? [];
return $this->repository->create([
/** @var \Pterodactyl\Models\EggVariable $model */
$model = EggVariable::query()->create([
'egg_id' => $egg,
'name' => $data['name'] ?? '',
'description' => $data['description'] ?? '',
@ -69,5 +59,6 @@ class VariableCreationService
'user_editable' => in_array('user_editable', $options),
'rules' => $data['rules'] ?? '',
]);
return $model;
}
}

View File

@ -3,6 +3,7 @@
namespace Pterodactyl\Services\Eggs\Variables;
use Illuminate\Support\Str;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Validation\Factory;
use Pterodactyl\Exceptions\DisplayException;
@ -14,11 +15,6 @@ class VariableUpdateService
{
use ValidatesValidationRules;
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface
*/
private $repository;
/**
* @var \Illuminate\Contracts\Validation\Factory
*/
@ -27,9 +23,8 @@ class VariableUpdateService
/**
* VariableUpdateService constructor.
*/
public function __construct(EggVariableRepositoryInterface $repository, Factory $validator)
public function __construct(Factory $validator)
{
$this->repository = $repository;
$this->validator = $validator;
}
@ -45,27 +40,22 @@ class VariableUpdateService
/**
* Update a specific egg variable.
*
* @return mixed
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function handle(EggVariable $variable, array $data)
public function handle(Egg $egg, array $data)
{
if (!is_null(array_get($data, 'env_variable'))) {
if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', EggVariable::RESERVED_ENV_NAMES))) {
throw new ReservedVariableNameException(trans('exceptions.service.variables.reserved_name', ['name' => array_get($data, 'env_variable')]));
}
$search = $this->repository->setColumns('id')->findCountWhere([
['env_variable', '=', $data['env_variable']],
['egg_id', '=', $variable->egg_id],
['id', '!=', $variable->id],
]);
$count = $egg->variables()
->where('egg_variables.env_variable',$data['env_variable'])
->where('egg_variables.id', '!=', $data['id'])
->count();
if ($search > 0) {
if ($count > 0) {
throw new DisplayException(trans('exceptions.service.variables.env_not_unique', ['name' => array_get($data, 'env_variable')]));
}
}
@ -80,13 +70,13 @@ class VariableUpdateService
$options = array_get($data, 'options') ?? [];
return $this->repository->withoutFreshModel()->update($variable->id, [
$egg->variables()->where('egg_variables.id', $data['id'])->update([
'name' => $data['name'] ?? '',
'description' => $data['description'] ?? '',
'env_variable' => $data['env_variable'] ?? '',
'default_value' => $data['default_value'] ?? '',
'user_viewable' => in_array('user_viewable', $options),
'user_editable' => in_array('user_editable', $options),
'user_viewable' => $data['user_viewable'],
'user_editable' => $data['user_editable'],
'rules' => $data['rules'] ?? '',
]);
}

View File

@ -4,7 +4,7 @@ import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg';
export default (egg: Partial<Egg>): Promise<Egg> => {
return new Promise((resolve, reject) => {
http.post(
'/api/application/eggs/',
'/api/application/eggs',
{
nest_id: egg.nestId,
name: egg.name,

View File

@ -0,0 +1,21 @@
import http from '@/api/http';
import { EggVariable, rawDataToEggVariable } from '@/api/admin/eggs/getEgg';
export default (eggId: number, variable: Omit<EggVariable, 'id' | 'eggId' | 'createdAt' | 'updatedAt'>): Promise<EggVariable> => {
return new Promise((resolve, reject) => {
http.post(
`/api/application/eggs/${eggId}`,
{
name: variable.name,
description: variable.description,
env_variable: variable.envVariable,
default_value: variable.defaultValue,
user_viewable: variable.userViewable,
user_editable: variable.userEditable,
rules: variable.rules,
},
)
.then(({ data }) => resolve(rawDataToEggVariable(data)))
.catch(reject);
});
};

View File

@ -12,12 +12,11 @@ export interface EggVariable {
userViewable: boolean;
userEditable: boolean;
rules: string;
required: boolean;
createdAt: Date;
updatedAt: Date;
}
const rawDataToEggVariable = ({ attributes }: FractalResponseData): EggVariable => ({
export const rawDataToEggVariable = ({ attributes }: FractalResponseData): EggVariable => ({
id: attributes.id,
eggId: attributes.egg_id,
name: attributes.name,
@ -27,7 +26,6 @@ const rawDataToEggVariable = ({ attributes }: FractalResponseData): EggVariable
userViewable: attributes.user_viewable,
userEditable: attributes.user_editable,
rules: attributes.rules,
required: attributes.required,
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),
});

View File

@ -0,0 +1,24 @@
import http from '@/api/http';
import { EggVariable, rawDataToEggVariable } from '@/api/admin/eggs/getEgg';
export default (eggId: number, variables: Omit<EggVariable, 'eggId' | 'createdAt' | 'updatedAt'>[]): Promise<EggVariable> => {
return new Promise((resolve, reject) => {
http.patch(
`/api/application/eggs/${eggId}/variables`,
variables.map(variable => {
return {
id: variable.id,
name: variable.name,
description: variable.description,
env_variable: variable.envVariable,
default_value: variable.defaultValue,
user_viewable: variable.userViewable,
user_editable: variable.userEditable,
rules: variable.rules,
};
}),
)
.then(({ data }) => resolve((data.data || []).map(rawDataToEggVariable)))
.catch(reject);
});
};

View File

@ -1,14 +1,17 @@
import { Form, Formik, useFormikContext } from 'formik';
import React from 'react';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import React, { useEffect } from 'react';
import tw from 'twin.macro';
import { object } from 'yup';
import { array, boolean, object, string } from 'yup';
import { Egg, EggVariable } from '@/api/admin/eggs/getEgg';
import updateEggVariables from '@/api/admin/eggs/updateEggVariables';
import AdminBox from '@/components/admin/AdminBox';
import Button from '@/components/elements/Button';
import Checkbox from '@/components/elements/Checkbox';
import Field, { FieldRow, TextareaField } from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import useFlash from '@/plugins/useFlash';
function EggVariableForm ({ variable: { name } }: { variable: EggVariable }) {
function EggVariableForm ({ variable: { name }, i }: { variable: EggVariable, i: number }) {
const { isSubmitting } = useFormikContext();
return (
@ -16,16 +19,16 @@ function EggVariableForm ({ variable: { name } }: { variable: EggVariable }) {
<SpinnerOverlay visible={isSubmitting}/>
<Field
id={'name'}
name={'name'}
id={`[${i}].name`}
name={`[${i}].name`}
label={'Name'}
type={'text'}
css={tw`mb-6`}
/>
<TextareaField
id={'description'}
name={'description'}
id={`[${i}].description`}
name={`[${i}].description`}
label={'Description'}
rows={3}
css={tw`mb-4`}
@ -33,15 +36,15 @@ function EggVariableForm ({ variable: { name } }: { variable: EggVariable }) {
<FieldRow>
<Field
id={'envVariable'}
name={'envVariable'}
id={`[${i}].envVariable`}
name={`[${i}].envVariable`}
label={'Environment Variable'}
type={'text'}
/>
<Field
id={'defaultValue'}
name={'defaultValue'}
id={`[${i}].defaultValue`}
name={`[${i}].defaultValue`}
label={'Default Value'}
type={'text'}
/>
@ -49,22 +52,22 @@ function EggVariableForm ({ variable: { name } }: { variable: EggVariable }) {
<div css={tw`flex flex-row mb-6`}>
<Checkbox
id={'userViewable'}
name={'userViewable'}
id={`[${i}].userViewable`}
name={`[${i}].userViewable`}
label={'User Viewable'}
/>
<Checkbox
id={'userEditable'}
name={'userEditable'}
id={`[${i}].userEditable`}
name={`[${i}].userEditable`}
label={'User Editable'}
css={tw`ml-auto`}
/>
</div>
<Field
id={'rules'}
name={'rules'}
id={`[${i}].rules`}
name={`[${i}].rules`}
label={'Validation Rules'}
type={'text'}
css={tw`mb-2`}
@ -74,23 +77,52 @@ function EggVariableForm ({ variable: { name } }: { variable: EggVariable }) {
}
export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
const submit = () => {
//
const { clearAndAddHttpError } = useFlash();
const submit = (values: EggVariable[], { setSubmitting }: FormikHelpers<EggVariable[]>) => {
updateEggVariables(egg.id, values)
.then(variables => console.log(variables))
.catch(error => clearAndAddHttpError({ key: 'egg', error }))
.then(() => setSubmitting(false));
};
useEffect(() => {
console.log(egg.relations?.variables || []);
}, []);
return (
<Formik
onSubmit={submit}
initialValues={{
}}
validationSchema={object().shape({
})}
initialValues={egg.relations?.variables || []}
validationSchema={array().of(
object().shape({
name: string().required().min(1).max(191),
description: string(),
envVariable: string().required().min(1).max(191),
defaultValue: string(),
userViewable: boolean().required(),
userEditable: boolean().required(),
rules: string().required(),
}),
)}
>
<Form>
<div css={tw`grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-6`}>
{egg.relations?.variables?.map(v => <EggVariableForm key={v.id} variable={v}/>)}
</div>
</Form>
{({ isSubmitting, isValid }) => (
<Form>
<div css={tw`flex flex-col mb-16`}>
<div css={tw`grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-6`}>
{egg.relations?.variables?.map((v, i) => <EggVariableForm key={v.id} i={i} variable={v}/>)}
</div>
<div css={tw`bg-neutral-700 rounded shadow-md py-2 pr-6 mt-6`}>
<div css={tw`flex flex-row`}>
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
Save Changes
</Button>
</div>
</div>
</div>
</Form>
)}
</Formik>
);
}

View File

@ -1,38 +1,23 @@
import Label from '@/components/elements/Label';
import { Field } from 'formik';
import React from 'react';
import { Field, FieldProps } from 'formik';
import Input from '@/components/elements/Input';
import tw from 'twin.macro';
import Label from '@/components/elements/Label';
interface Props {
id: string;
name: string;
label?: string;
className?: string;
}
type OmitFields = 'ref' | 'name' | 'value' | 'type';
type InputProps = Omit<JSX.IntrinsicElements['input'], OmitFields>;
const Checkbox = ({ name, label, className, ...props }: Props & InputProps) => (
<Field name={name}>
{({ field }: FieldProps) => {
return (
<div css={tw`flex flex-row`} className={className}>
<Input
{...field}
{...props}
css={tw`w-5 h-5 mr-2`}
type={'checkbox'}
/>
{label &&
<div css={tw`flex-1`}>
<Label noBottomSpacing>{label}</Label>
</div>}
</div>
);
}}
</Field>
const Checkbox = ({ id, name, label, className }: Props) => (
<div css={tw`flex flex-row`} className={className}>
<Field type={'checkbox'} id={id} name={name} css={[ label && tw`mr-2` ]}/>
{label &&
<div css={tw`flex-1`}>
<Label noBottomSpacing>{label}</Label>
</div>}
</div>
);
export default Checkbox;

View File

@ -35,8 +35,10 @@ Route::group(['prefix' => '/eggs'], function () {
Route::get('/{egg}', [\Pterodactyl\Http\Controllers\Api\Application\Eggs\EggController::class, 'view']);
Route::post('/', [\Pterodactyl\Http\Controllers\Api\Application\Eggs\EggController::class, 'store']);
Route::post('/{egg}/variables', [\Pterodactyl\Http\Controllers\Api\Application\Eggs\EggVariableController::class, 'store']);
Route::patch('/{egg}', [\Pterodactyl\Http\Controllers\Api\Application\Eggs\EggController::class, 'update']);
Route::patch('/{egg}/variables', [\Pterodactyl\Http\Controllers\Api\Application\Eggs\EggVariableController::class, 'update']);
Route::delete('/{egg}', [\Pterodactyl\Http\Controllers\Api\Application\Eggs\EggController::class, 'delete']);
});