From 1eed25dcc75a2cf9d0b951cd1baef05d566a26a3 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 3 Oct 2021 14:23:06 -0600 Subject: [PATCH] ui(admin): finish egg variable editing --- .../Eggs/EggVariableController.php | 65 ++++++++++++++ .../Variables/StoreEggVariableRequest.php | 22 +++++ .../Variables/UpdateEggVariablesRequest.php | 25 ++++++ app/Models/EggVariable.php | 1 - .../Variables/VariableCreationService.php | 23 ++--- .../Eggs/Variables/VariableUpdateService.php | 32 +++---- resources/scripts/api/admin/eggs/createEgg.ts | 2 +- .../api/admin/eggs/createEggVariable.ts | 21 +++++ resources/scripts/api/admin/eggs/getEgg.ts | 4 +- .../api/admin/eggs/updateEggVariables.ts | 24 +++++ .../nests/eggs/EggVariablesContainer.tsx | 90 +++++++++++++------ .../scripts/components/elements/Checkbox.tsx | 37 +++----- routes/api-application.php | 2 + 13 files changed, 251 insertions(+), 97 deletions(-) create mode 100644 app/Http/Controllers/Api/Application/Eggs/EggVariableController.php create mode 100644 app/Http/Requests/Api/Application/Eggs/Variables/StoreEggVariableRequest.php create mode 100644 app/Http/Requests/Api/Application/Eggs/Variables/UpdateEggVariablesRequest.php create mode 100644 resources/scripts/api/admin/eggs/createEggVariable.ts create mode 100644 resources/scripts/api/admin/eggs/updateEggVariables.ts diff --git a/app/Http/Controllers/Api/Application/Eggs/EggVariableController.php b/app/Http/Controllers/Api/Application/Eggs/EggVariableController.php new file mode 100644 index 000000000..7ffb86399 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Eggs/EggVariableController.php @@ -0,0 +1,65 @@ +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(); + } +} diff --git a/app/Http/Requests/Api/Application/Eggs/Variables/StoreEggVariableRequest.php b/app/Http/Requests/Api/Application/Eggs/Variables/StoreEggVariableRequest.php new file mode 100644 index 000000000..9c674a9d8 --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/Variables/StoreEggVariableRequest.php @@ -0,0 +1,22 @@ + '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', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Eggs/Variables/UpdateEggVariablesRequest.php b/app/Http/Requests/Api/Application/Eggs/Variables/UpdateEggVariablesRequest.php new file mode 100644 index 000000000..363a2fece --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/Variables/UpdateEggVariablesRequest.php @@ -0,0 +1,25 @@ + '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', + ]; + } +} diff --git a/app/Models/EggVariable.php b/app/Models/EggVariable.php index 98e3da2c9..bb5938292 100644 --- a/app/Models/EggVariable.php +++ b/app/Models/EggVariable.php @@ -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 * diff --git a/app/Services/Eggs/Variables/VariableCreationService.php b/app/Services/Eggs/Variables/VariableCreationService.php index fa758265b..11fafb44b 100644 --- a/app/Services/Eggs/Variables/VariableCreationService.php +++ b/app/Services/Eggs/Variables/VariableCreationService.php @@ -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; } } diff --git a/app/Services/Eggs/Variables/VariableUpdateService.php b/app/Services/Eggs/Variables/VariableUpdateService.php index da4426c33..866f2157f 100644 --- a/app/Services/Eggs/Variables/VariableUpdateService.php +++ b/app/Services/Eggs/Variables/VariableUpdateService.php @@ -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'] ?? '', ]); } diff --git a/resources/scripts/api/admin/eggs/createEgg.ts b/resources/scripts/api/admin/eggs/createEgg.ts index 5a2f8c964..f269d696e 100644 --- a/resources/scripts/api/admin/eggs/createEgg.ts +++ b/resources/scripts/api/admin/eggs/createEgg.ts @@ -4,7 +4,7 @@ import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg'; export default (egg: Partial): Promise => { return new Promise((resolve, reject) => { http.post( - '/api/application/eggs/', + '/api/application/eggs', { nest_id: egg.nestId, name: egg.name, diff --git a/resources/scripts/api/admin/eggs/createEggVariable.ts b/resources/scripts/api/admin/eggs/createEggVariable.ts new file mode 100644 index 000000000..4226eef15 --- /dev/null +++ b/resources/scripts/api/admin/eggs/createEggVariable.ts @@ -0,0 +1,21 @@ +import http from '@/api/http'; +import { EggVariable, rawDataToEggVariable } from '@/api/admin/eggs/getEgg'; + +export default (eggId: number, variable: Omit): Promise => { + 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); + }); +}; diff --git a/resources/scripts/api/admin/eggs/getEgg.ts b/resources/scripts/api/admin/eggs/getEgg.ts index 48e139301..be0ca0395 100644 --- a/resources/scripts/api/admin/eggs/getEgg.ts +++ b/resources/scripts/api/admin/eggs/getEgg.ts @@ -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), }); diff --git a/resources/scripts/api/admin/eggs/updateEggVariables.ts b/resources/scripts/api/admin/eggs/updateEggVariables.ts new file mode 100644 index 000000000..498fda0ba --- /dev/null +++ b/resources/scripts/api/admin/eggs/updateEggVariables.ts @@ -0,0 +1,24 @@ +import http from '@/api/http'; +import { EggVariable, rawDataToEggVariable } from '@/api/admin/eggs/getEgg'; + +export default (eggId: number, variables: Omit[]): Promise => { + 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); + }); +}; diff --git a/resources/scripts/components/admin/nests/eggs/EggVariablesContainer.tsx b/resources/scripts/components/admin/nests/eggs/EggVariablesContainer.tsx index 41388a07e..f771a6f25 100644 --- a/resources/scripts/components/admin/nests/eggs/EggVariablesContainer.tsx +++ b/resources/scripts/components/admin/nests/eggs/EggVariablesContainer.tsx @@ -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 }) { @@ -49,22 +52,22 @@ function EggVariableForm ({ variable: { name } }: { variable: EggVariable }) {
{ - // + const { clearAndAddHttpError } = useFlash(); + + const submit = (values: EggVariable[], { setSubmitting }: FormikHelpers) => { + 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 ( -
-
- {egg.relations?.variables?.map(v => )} -
-
+ {({ isSubmitting, isValid }) => ( +
+
+
+ {egg.relations?.variables?.map((v, i) => )} +
+ +
+
+ +
+
+
+
+ )}
); } diff --git a/resources/scripts/components/elements/Checkbox.tsx b/resources/scripts/components/elements/Checkbox.tsx index b3954e708..25c9da037 100644 --- a/resources/scripts/components/elements/Checkbox.tsx +++ b/resources/scripts/components/elements/Checkbox.tsx @@ -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; - -const Checkbox = ({ name, label, className, ...props }: Props & InputProps) => ( - - {({ field }: FieldProps) => { - return ( -
- - {label && -
- -
} -
- ); - }} -
+const Checkbox = ({ id, name, label, className }: Props) => ( +
+ + {label && +
+ +
} +
); export default Checkbox; diff --git a/routes/api-application.php b/routes/api-application.php index d9000f59f..67c3a5c50 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -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']); });