ui(admin): new egg page

This commit is contained in:
Matthew Penner 2021-10-23 13:13:25 -06:00
parent 336923ec18
commit cddf2ce41c
No known key found for this signature in database
GPG key ID: BAB67850901908A8
15 changed files with 210 additions and 84 deletions

View file

@ -2,10 +2,12 @@
namespace Pterodactyl\Http\Controllers\Api\Application\Eggs; namespace Pterodactyl\Http\Controllers\Api\Application\Eggs;
use Ramsey\Uuid\Uuid;
use Pterodactyl\Models\Egg; use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest; use Pterodactyl\Models\Nest;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\QueryBuilder;
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;
@ -56,7 +58,16 @@ class EggController extends ApplicationApiController
*/ */
public function store(StoreEggRequest $request): JsonResponse public function store(StoreEggRequest $request): JsonResponse
{ {
$egg = Egg::query()->create($request->validated()); $validated = $request->validated();
Log::info(json_encode($validated, JSON_PRETTY_PRINT));
$merged = array_merge($validated, [
'uuid' => Uuid::uuid4()->toString(),
// TODO: allow this to be set in the request, and default to config value if null or not present.
'author' => config('pterodactyl.service.author'),
]);
Log::info(json_encode($merged, JSON_PRETTY_PRINT));
$egg = Egg::query()->create($merged);
return $this->fractal->item($egg) return $this->fractal->item($egg)
->transformWith(EggTransformer::class) ->transformWith(EggTransformer::class)

View file

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

View file

@ -10,16 +10,16 @@ class UpdateEggRequest extends StoreEggRequest
'nest_id' => 'sometimes|numeric|exists:nests,id', 'nest_id' => 'sometimes|numeric|exists:nests,id',
'name' => 'sometimes|string|max:191', 'name' => 'sometimes|string|max:191',
'description' => 'sometimes|string|nullable', 'description' => 'sometimes|string|nullable',
'features' => 'sometimes|array|nullable', 'features' => 'sometimes|array',
'docker_images' => 'sometimes|required|array|min:1', 'docker_images' => 'sometimes|array|min:1',
'docker_images.*' => 'sometimes|string', 'docker_images.*' => 'sometimes|string',
'file_denylist' => 'sometimes|array|nullable', 'file_denylist' => 'sometimes|array|nullable',
'file_denylist.*' => 'sometimes|string', 'file_denylist.*' => 'sometimes|string',
'config_files' => 'sometimes|nullable|json', 'config_files' => 'sometimes|nullable|json',
'config_startup' => 'sometimes|nullable|json', 'config_startup' => 'sometimes|nullable|json',
'config_stop' => 'sometimes|nullable|string|max:191', 'config_stop' => 'sometimes|nullable|string|max:191',
'config_from' => 'sometimes|nullable|numeric|exists:eggs,id', // 'config_from' => 'sometimes|nullable|numeric|exists:eggs,id',
'startup' => 'sometimes|nullable|string', 'startup' => 'sometimes|string',
'script_container' => 'sometimes|string', 'script_container' => 'sometimes|string',
'script_entry' => 'sometimes|string', 'script_entry' => 'sometimes|string',
'script_install' => 'sometimes|string', 'script_install' => 'sometimes|string',

View file

@ -16,7 +16,6 @@ namespace Pterodactyl\Models;
* @property array|null $file_denylist * @property array|null $file_denylist
* @property string|null $config_files * @property string|null $config_files
* @property string|null $config_startup * @property string|null $config_startup
* @property string|null $config_logs
* @property string|null $config_stop * @property string|null $config_stop
* @property int|null $config_from * @property int|null $config_from
* @property string|null $startup * @property string|null $startup
@ -32,7 +31,6 @@ namespace Pterodactyl\Models;
* @property string $copy_script_container * @property string $copy_script_container
* @property string|null $inherit_config_files * @property string|null $inherit_config_files
* @property string|null $inherit_config_startup * @property string|null $inherit_config_startup
* @property string|null $inherit_config_logs
* @property string|null $inherit_config_stop * @property string|null $inherit_config_stop
* @property string $inherit_file_denylist * @property string $inherit_file_denylist
* @property array|null $inherit_features * @property array|null $inherit_features
@ -75,14 +73,16 @@ class Egg extends Model
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'nest_id',
'uuid',
'name', 'name',
'description', 'description',
'features', 'features',
'author',
'docker_images', 'docker_images',
'file_denylist', 'file_denylist',
'config_files', 'config_files',
'config_startup', 'config_startup',
'config_logs',
'config_stop', 'config_stop',
'config_from', 'config_from',
'startup', 'startup',
@ -123,7 +123,6 @@ class Egg extends Model
'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id', 'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id',
'config_stop' => 'required_without:config_from|nullable|string|max:191', 'config_stop' => 'required_without:config_from|nullable|string|max:191',
'config_startup' => 'required_without:config_from|nullable|json', 'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json', 'config_files' => 'required_without:config_from|nullable|json',
'update_url' => 'sometimes|nullable|string', 'update_url' => 'sometimes|nullable|string',
]; ];
@ -136,7 +135,6 @@ class Egg extends Model
'file_denylist' => null, 'file_denylist' => null,
'config_stop' => null, 'config_stop' => null,
'config_startup' => null, 'config_startup' => null,
'config_logs' => null,
'config_files' => null, 'config_files' => null,
'update_url' => null, 'update_url' => null,
]; ];
@ -214,20 +212,6 @@ class Egg extends Model
return $this->configFrom->config_startup; return $this->configFrom->config_startup;
} }
/**
* Return the log reading configuration for an egg.
*
* @return string
*/
public function getInheritConfigLogsAttribute()
{
if (!is_null($this->config_logs) || is_null($this->config_from)) {
return $this->config_logs;
}
return $this->configFrom->config_logs;
}
/** /**
* Return the stop command configuration for an egg. * Return the stop command configuration for an egg.
* *

View file

@ -50,7 +50,6 @@ class EggExporterService
'config' => [ 'config' => [
'files' => $egg->inherit_config_files, 'files' => $egg->inherit_config_files,
'startup' => $egg->inherit_config_startup, 'startup' => $egg->inherit_config_startup,
'logs' => $egg->inherit_config_logs,
'stop' => $egg->inherit_config_stop, 'stop' => $egg->inherit_config_stop,
], ],
'scripts' => [ 'scripts' => [

View file

@ -151,7 +151,6 @@ class EggImporterService
'update_url' => Arr::get($parsed, 'meta.update_url'), 'update_url' => Arr::get($parsed, 'meta.update_url'),
'config_files' => Arr::get($parsed, 'config.files'), 'config_files' => Arr::get($parsed, 'config.files'),
'config_startup' => Arr::get($parsed, 'config.startup'), 'config_startup' => Arr::get($parsed, 'config.startup'),
'config_logs' => Arr::get($parsed, 'config.logs'),
'config_stop' => Arr::get($parsed, 'config.stop'), 'config_stop' => Arr::get($parsed, 'config.stop'),
'startup' => Arr::get($parsed, 'startup'), 'startup' => Arr::get($parsed, 'startup'),
'script_install' => Arr::get($parsed, 'scripts.installation.script'), 'script_install' => Arr::get($parsed, 'scripts.installation.script'),

View file

@ -74,7 +74,6 @@ class EggUpdateImporterService
'docker_images' => object_get($parsed, 'images') ?? [object_get($parsed, 'image')], 'docker_images' => object_get($parsed, 'images') ?? [object_get($parsed, 'image')],
'config_files' => object_get($parsed, 'config.files'), 'config_files' => object_get($parsed, 'config.files'),
'config_startup' => object_get($parsed, 'config.startup'), 'config_startup' => object_get($parsed, 'config.startup'),
'config_logs' => object_get($parsed, 'config.logs'),
'config_stop' => object_get($parsed, 'config.stop'), 'config_stop' => object_get($parsed, 'config.stop'),
'startup' => object_get($parsed, 'startup'), 'startup' => object_get($parsed, 'startup'),
'script_install' => object_get($parsed, 'scripts.installation.script'), 'script_install' => object_get($parsed, 'scripts.installation.script'),

View file

@ -41,10 +41,9 @@ class EggTransformer extends Transformer
'docker_image' => count($model->docker_images) > 0 ? $model->docker_images[0] : '', 'docker_image' => count($model->docker_images) > 0 ? $model->docker_images[0] : '',
'docker_images' => $model->docker_images, 'docker_images' => $model->docker_images,
'config' => [ 'config' => [
'files' => json_decode($model->config_files, true), 'files' => json_decode($model->config_files),
'startup' => json_decode($model->config_startup, true), 'startup' => json_decode($model->config_startup),
'stop' => $model->config_stop, 'stop' => $model->config_stop,
'logs' => json_decode($model->config_logs, true),
'file_denylist' => $model->file_denylist, 'file_denylist' => $model->file_denylist,
'extends' => $model->config_from, 'extends' => $model->config_from,
], ],
@ -106,7 +105,6 @@ class EggTransformer extends Transformer
'files' => json_decode($model->inherit_config_files), 'files' => json_decode($model->inherit_config_files),
'startup' => json_decode($model->inherit_config_startup), 'startup' => json_decode($model->inherit_config_startup),
'stop' => $model->inherit_config_stop, 'stop' => $model->inherit_config_stop,
'logs' => json_decode($model->inherit_config_logs),
]; ];
}); });
} }

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class DropConfigLogsColumnFromEggsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('eggs', function (Blueprint $table) {
$table->dropColumn('config_logs');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('eggs', function (Blueprint $table) {
$table->text('config_logs')->nullable()->after('docker_image');
});
}
}

View file

@ -1,7 +1,9 @@
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 (egg: Partial<Egg>): Promise<Egg> => { type Egg2 = Omit<Omit<Partial<Egg>, 'configFiles'>, 'configStartup'> & { configFiles: string, configStartup: string };
export default (egg: Partial<Egg2>): Promise<Egg> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post( http.post(
'/api/application/eggs', '/api/application/eggs',

View file

@ -196,9 +196,9 @@ export const Loading = () => {
); );
}; };
export const NoItems = () => { export const NoItems = ({ className }: { className?: string }) => {
return ( return (
<div css={tw`w-full flex flex-col items-center justify-center py-6 px-8`}> <div css={tw`w-full flex flex-col items-center justify-center py-6 px-8`} className={className}>
<div css={tw`h-48 flex`}> <div css={tw`h-48 flex`}>
<img src={'/assets/svgs/not_found.svg'} alt={'No Items'} css={tw`h-full select-none`}/> <img src={'/assets/svgs/not_found.svg'} alt={'No Items'} css={tw`h-full select-none`}/>
</div> </div>
@ -227,7 +227,8 @@ export const ContentWrapper = ({ checked, onSelectAllClick, onSearch, children }
} }
setLoading(true); setLoading(true);
onSearch(query).then(() => setLoading(false)); onSearch(query)
.then(() => setLoading(false));
}, 200), }, 200),
[], [],
); );

View file

@ -1,8 +1,51 @@
import React from 'react'; import createEgg from '@/api/admin/eggs/createEgg';
import { EggImageContainer, EggInformationContainer, EggLifecycleContainer, EggProcessContainer, EggProcessContainerRef, EggStartupContainer } from '@/components/admin/nests/eggs/EggSettingsContainer';
import Button from '@/components/elements/Button';
import FlashMessageRender from '@/components/FlashMessageRender';
import useFlash from '@/plugins/useFlash';
import { Form, Formik, FormikHelpers } from 'formik';
import React, { useRef } from 'react';
import { useParams } from 'react-router';
import { useHistory } from 'react-router-dom';
import tw from 'twin.macro'; import tw from 'twin.macro';
import AdminContentBlock from '@/components/admin/AdminContentBlock'; import AdminContentBlock from '@/components/admin/AdminContentBlock';
import { object } from 'yup';
interface Values {
name: string;
description: string;
startup: string;
dockerImages: string;
configStop: string;
configStartup: string;
configFiles: string;
}
export default () => { export default () => {
const params = useParams<{ nestId: string }>();
const history = useHistory();
const { clearFlashes, clearAndAddHttpError } = useFlash();
const ref = useRef<EggProcessContainerRef>();
const submit = async (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('egg:create');
const nestId = Number(params.nestId);
values.configStartup = await ref.current?.getStartupConfiguration() || '';
values.configFiles = await ref.current?.getFilesConfiguration() || '';
createEgg({ ...values, dockerImages: values.dockerImages.split('\n'), nestId })
.then(egg => history.push(`/admin/nests/${nestId}/eggs/${egg.id}`))
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'egg:create', error });
})
.then(() => setSubmitting(false));
};
return ( return (
<AdminContentBlock title={'New Egg'}> <AdminContentBlock title={'New Egg'}>
<div css={tw`w-full flex flex-row items-center mb-8`}> <div css={tw`w-full flex flex-row items-center mb-8`}>
@ -11,6 +54,48 @@ export default () => {
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>Add a new egg to the panel.</p> <p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>Add a new egg to the panel.</p>
</div> </div>
</div> </div>
<FlashMessageRender key={'egg:create'} css={tw`mb-4`}/>
<Formik
onSubmit={submit}
initialValues={{
name: '',
description: '',
startup: '',
dockerImages: '',
configStop: '',
configStartup: '{}',
configFiles: '{}',
}}
validationSchema={object().shape({
})}
>
{({ isSubmitting, isValid }) => (
<Form>
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6`}>
<EggInformationContainer/>
</div>
<EggStartupContainer css={tw`mb-6`}/>
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6`}>
<EggImageContainer/>
<EggLifecycleContainer/>
</div>
<EggProcessContainer ref={ref} css={tw`mb-6`}/>
<div css={tw`bg-neutral-700 rounded shadow-md py-2 px-6 mb-16`}>
<div css={tw`flex flex-row`}>
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
Create
</Button>
</div>
</div>
</Form>
)}
</Formik>
</AdminContentBlock> </AdminContentBlock>
); );
}; };

View file

@ -18,7 +18,7 @@ import tw from 'twin.macro';
import { object } from 'yup'; import { object } from 'yup';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
function EggInformationContainer () { export function EggInformationContainer () {
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
return ( return (
@ -72,7 +72,7 @@ function EggDetailsContainer ({ egg }: { egg: Egg }) {
); );
} }
function EggStartupContainer ({ className }: { className?: string }) { export function EggStartupContainer ({ className }: { className?: string }) {
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
return ( return (
@ -90,7 +90,7 @@ function EggStartupContainer ({ className }: { className?: string }) {
); );
} }
function EggImageContainer () { export function EggImageContainer () {
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
return ( return (
@ -107,7 +107,7 @@ function EggImageContainer () {
); );
} }
function EggLifecycleContainer () { export function EggLifecycleContainer () {
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
return ( return (
@ -115,8 +115,8 @@ function EggLifecycleContainer () {
<SpinnerOverlay visible={isSubmitting}/> <SpinnerOverlay visible={isSubmitting}/>
<Field <Field
id={'stopCommand'} id={'configStop'}
name={'stopCommand'} name={'configStop'}
label={'Stop Command'} label={'Stop Command'}
type={'text'} type={'text'}
css={tw`mb-1`} css={tw`mb-1`}
@ -127,17 +127,16 @@ function EggLifecycleContainer () {
interface EggProcessContainerProps { interface EggProcessContainerProps {
className?: string; className?: string;
egg: Egg;
} }
interface EggProcessContainerRef { export interface EggProcessContainerRef {
getStartupConfiguration: () => Promise<string | null>; getStartupConfiguration: () => Promise<string | null>;
getFilesConfiguration: () => Promise<string | null>; getFilesConfiguration: () => Promise<string | null>;
} }
const EggProcessContainer = forwardRef<any, EggProcessContainerProps>( export const EggProcessContainer = forwardRef<any, EggProcessContainerProps>(
function EggProcessContainer ({ className, egg }, ref) { function EggProcessContainer ({ className }, ref) {
const { isSubmitting } = useFormikContext(); const { isSubmitting, values } = useFormikContext<Values>();
let fetchStartupConfiguration: (() => Promise<string>) | null = null; let fetchStartupConfiguration: (() => Promise<string>) | null = null;
let fetchFilesConfiguration: (() => Promise<string>) | null = null; let fetchFilesConfiguration: (() => Promise<string>) | null = null;
@ -166,7 +165,7 @@ const EggProcessContainer = forwardRef<any, EggProcessContainerProps>(
<Label>Startup Configuration</Label> <Label>Startup Configuration</Label>
<Editor <Editor
mode={jsonLanguage} mode={jsonLanguage}
initialContent={JSON.stringify(egg.configStartup, null, '\t') || ''} initialContent={values.configStartup}
overrides={tw`h-32 rounded`} overrides={tw`h-32 rounded`}
fetchContent={value => { fetchContent={value => {
fetchStartupConfiguration = value; fetchStartupConfiguration = value;
@ -178,7 +177,7 @@ const EggProcessContainer = forwardRef<any, EggProcessContainerProps>(
<Label>Configuration Files</Label> <Label>Configuration Files</Label>
<Editor <Editor
mode={jsonLanguage} mode={jsonLanguage}
initialContent={JSON.stringify(egg.configFiles, null, '\t') || ''} initialContent={values.configFiles}
overrides={tw`h-48 rounded`} overrides={tw`h-48 rounded`}
fetchContent={value => { fetchContent={value => {
fetchFilesConfiguration = value; fetchFilesConfiguration = value;
@ -195,7 +194,7 @@ interface Values {
description: string; description: string;
startup: string; startup: string;
dockerImages: string; dockerImages: string;
stopCommand: string; configStop: string;
configStartup: string; configStartup: string;
configFiles: string; configFiles: string;
} }
@ -229,9 +228,9 @@ export default function EggSettingsContainer ({ egg }: { egg: Egg }) {
description: egg.description || '', description: egg.description || '',
startup: egg.startup, startup: egg.startup,
dockerImages: egg.dockerImages.join('\n'), dockerImages: egg.dockerImages.join('\n'),
stopCommand: egg.configStop || '', configStop: egg.configStop || '',
configStartup: '', configStartup: JSON.stringify(egg.configStartup, null, '\t') || '',
configFiles: '', configFiles: JSON.stringify(egg.configFiles, null, '\t') || '',
}} }}
validationSchema={object().shape({ validationSchema={object().shape({
})} })}
@ -250,17 +249,13 @@ export default function EggSettingsContainer ({ egg }: { egg: Egg }) {
<EggLifecycleContainer/> <EggLifecycleContainer/>
</div> </div>
<EggProcessContainer <EggProcessContainer ref={ref} css={tw`mb-6`}/>
ref={ref}
egg={egg}
css={tw`mb-6`}
/>
<div css={tw`bg-neutral-700 rounded shadow-md py-2 px-6 mb-16`}> <div css={tw`bg-neutral-700 rounded shadow-md py-2 px-6 mb-16`}>
<div css={tw`flex flex-row`}> <div css={tw`flex flex-row`}>
<EggDeleteButton <EggDeleteButton
eggId={egg.id} eggId={egg.id}
onDeleted={() => history.push('/admin/nests')} onDeleted={() => history.push(`/admin/nests/${egg.nestId}`)}
/> />
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}> <Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
Save Changes Save Changes

View file

@ -1,4 +1,5 @@
import deleteEggVariable from '@/api/admin/eggs/deleteEggVariable'; import deleteEggVariable from '@/api/admin/eggs/deleteEggVariable';
import { NoItems } from '@/components/admin/AdminTable';
import ConfirmationModal from '@/components/elements/ConfirmationModal'; import ConfirmationModal from '@/components/elements/ConfirmationModal';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import React, { useState } from 'react'; import React, { useState } from 'react';
@ -160,28 +161,32 @@ export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
{({ isSubmitting, isValid }) => ( {({ isSubmitting, isValid }) => (
<Form> <Form>
<div css={tw`flex flex-col mb-16`}> <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?.length === 0 ?
{egg.relations?.variables?.map((v, i) => ( <NoItems css={tw`bg-neutral-700 rounded-md shadow-md`}/>
<EggVariableBox :
key={i} <div css={tw`grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-6`}>
prefix={`[${i}].`} {egg.relations?.variables?.map((v, i) => (
variable={v} <EggVariableBox
onDeleteClick={(success) => { key={i}
deleteEggVariable(egg.id, v.id) prefix={`[${i}].`}
.then(async () => { variable={v}
await mutate(egg => ({ onDeleteClick={(success) => {
...egg!, deleteEggVariable(egg.id, v.id)
relations: { .then(async () => {
variables: egg!.relations.variables!.filter(v2 => v.id === v2.id), await mutate(egg => ({
}, ...egg!,
})); relations: {
success(); variables: egg!.relations.variables!.filter(v2 => v.id === v2.id),
}) },
.catch(error => clearAndAddHttpError({ key: 'egg', error })); }));
}} success();
/> })
))} .catch(error => clearAndAddHttpError({ key: 'egg', error }));
</div> }}
/>
))}
</div>
}
<div css={tw`bg-neutral-700 rounded shadow-md py-2 px-4 mt-6`}> <div css={tw`bg-neutral-700 rounded shadow-md py-2 px-4 mt-6`}>
<div css={tw`flex flex-row`}> <div css={tw`flex flex-row`}>

View file

@ -80,7 +80,7 @@ export default function NewVariableButton ({ eggId }: { eggId: number }) {
</Formik> </Formik>
<Button type={'button'} color={'green'} onClick={() => setVisible(true)}> <Button type={'button'} color={'green'} onClick={() => setVisible(true)}>
Add a fucking variable New Variable
</Button> </Button>
</> </>
); );