ui(admin): fix egg variables

This commit is contained in:
Matthew Penner 2021-11-04 14:32:42 -06:00
parent f7c824743f
commit ce0bc477c2
No known key found for this signature in database
GPG key ID: BAB67850901908A8
12 changed files with 157 additions and 100 deletions

View file

@ -55,8 +55,8 @@ class VariableCreationService
'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

@ -2,6 +2,9 @@ import { Model, UUID, WithRelationships, withRelationships } from '@/api/admin/i
import { Nest } from '@/api/admin/nest';
import http, { QueryBuilderParams, withQueryBuilderParams } from '@/api/http';
import { AdminTransformers } from '@/api/admin/transformers';
import { AxiosError } from 'axios';
import { useRouteMatch } from 'react-router-dom';
import useSWR, { SWRResponse } from 'swr';
export interface Egg extends Model {
id: number;
@ -39,16 +42,22 @@ export interface EggVariable extends Model {
defaultValue: string;
isUserViewable: boolean;
isUserEditable: boolean;
isRequired: boolean;
// isRequired: boolean;
rules: string;
createdAt: Date;
updatedAt: Date;
}
/**
* A standard API response with the minimum viable details for the frontend
* to correctly render a egg.
*/
type LoadedEgg = WithRelationships<Egg, 'nest' | 'variables'>;
/**
* Gets a single egg from the database and returns it.
*/
export const getEgg = async (id: number | string): Promise<WithRelationships<Egg, 'nest' | 'variables'>> => {
export const getEgg = async (id: number | string): Promise<LoadedEgg> => {
const { data } = await http.get(`/api/application/eggs/${id}`, {
params: {
include: [ 'nest', 'variables' ],
@ -73,3 +82,16 @@ export const exportEgg = async (eggId: number): Promise<Record<string, any>> =>
const { data } = await http.get(`/api/application/eggs/${eggId}/export`);
return data;
};
/**
* Returns an SWR instance by automatically loading in the server for the currently
* loaded route match in the admin area.
*/
export const useEggFromRoute = (): SWRResponse<LoadedEgg, AxiosError> => {
const { params } = useRouteMatch<{ id: string }>();
return useSWR(`/api/application/eggs/${params.id}`, async () => getEgg(params.id), {
revalidateOnMount: false,
revalidateOnFocus: false,
});
};

View file

@ -1,21 +1,22 @@
import http from '@/api/http';
import { EggVariable, rawDataToEggVariable } from '@/api/admin/eggs/getEgg';
import { EggVariable } from '@/api/admin/egg';
import { AdminTransformers } from '@/api/admin/transformers';
export default (eggId: number, variable: Omit<EggVariable, 'id' | 'eggId' | 'createdAt' | 'updatedAt'>): Promise<EggVariable> => {
return new Promise((resolve, reject) => {
http.post(
`/api/application/eggs/${eggId}/variables`,
{
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);
});
export type CreateEggVariable = Omit<EggVariable, 'id' | 'eggId' | 'createdAt' | 'updatedAt' | 'relationships'>;
export default async (eggId: number, variable: CreateEggVariable): Promise<EggVariable> => {
const { data } = await http.post(
`/api/application/eggs/${eggId}/variables`,
{
name: variable.name,
description: variable.description,
env_variable: variable.environmentVariable,
default_value: variable.defaultValue,
user_viewable: variable.isUserViewable,
user_editable: variable.isUserEditable,
rules: variable.rules,
},
);
return AdminTransformers.toEggVariable(data);
};

View file

@ -1,22 +1,21 @@
import http from '@/api/http';
import { EggVariable, rawDataToEggVariable } from '@/api/admin/eggs/getEgg';
import { EggVariable } from '@/api/admin/egg';
import { AdminTransformers } from '@/api/admin/transformers';
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 => ({
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);
});
export default async (eggId: number, variables: Omit<EggVariable, 'eggId' | 'createdAt' | 'updatedAt'>[]): Promise<EggVariable[]> => {
const { data } = await http.patch(
`/api/application/eggs/${eggId}/variables`,
variables.map(variable => ({
id: variable.id,
name: variable.name,
description: variable.description,
env_variable: variable.environmentVariable,
default_value: variable.defaultValue,
user_viewable: variable.isUserViewable,
user_editable: variable.isUserEditable,
rules: variable.rules,
})),
);
return data.data.map(AdminTransformers.toEggVariable);
};

View file

@ -168,7 +168,7 @@ export class AdminTransformers {
defaultValue: attributes.default_value,
isUserViewable: attributes.user_viewable,
isUserEditable: attributes.user_editable,
isRequired: attributes.required,
// isRequired: attributes.required,
rules: attributes.rules,
createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at),

View file

@ -1,4 +1,4 @@
import { Egg } from '@/api/admin/eggs/getEgg';
import { useEggFromRoute } from '@/api/admin/egg';
import updateEgg from '@/api/admin/eggs/updateEgg';
import Field from '@/components/elements/Field';
import useFlash from '@/plugins/useFlash';
@ -18,9 +18,15 @@ interface Values {
scriptInstall: string;
}
export default function EggInstallContainer ({ egg }: { egg: Egg }) {
export default function EggInstallContainer () {
const { clearFlashes, clearAndAddHttpError } = useFlash();
const { data: egg } = useEggFromRoute();
if (!egg) {
return null;
}
let fetchFileContent: (() => Promise<string>) | null = null;
const submit = async (values: Values, { setSubmitting }: FormikHelpers<Values>) => {

View file

@ -1,10 +1,11 @@
import { useEggFromRoute } from '@/api/admin/egg';
import EggInstallContainer from '@/components/admin/nests/eggs/EggInstallContainer';
import EggVariablesContainer from '@/components/admin/nests/eggs/EggVariablesContainer';
import React from 'react';
import useFlash from '@/plugins/useFlash';
import React, { useEffect } from 'react';
import { useLocation } from 'react-router';
import tw from 'twin.macro';
import { Route, Switch, useRouteMatch } from 'react-router-dom';
import getEgg from '@/api/admin/eggs/getEgg';
import AdminContentBlock from '@/components/admin/AdminContentBlock';
import Spinner from '@/components/elements/Spinner';
import FlashMessageRender from '@/components/FlashMessageRender';
@ -13,18 +14,24 @@ import EggSettingsContainer from '@/components/admin/nests/eggs/EggSettingsConta
const EggRouter = () => {
const location = useLocation();
const match = useRouteMatch<{ id?: string }>();
const match = useRouteMatch();
const { data: egg } = getEgg(Number(match.params?.id));
const { clearFlashes, clearAndAddHttpError } = useFlash();
const { data: egg, error, isValidating, mutate } = useEggFromRoute();
if (egg === undefined) {
useEffect(() => {
mutate();
}, []);
useEffect(() => {
if (!error) clearFlashes('egg');
if (error) clearAndAddHttpError({ error, key: 'egg' });
}, [ error ]);
if (!egg || (error && isValidating)) {
return (
<AdminContentBlock>
<FlashMessageRender byKey={'egg'} css={tw`mb-4`}/>
<div css={tw`w-full flex flex-col items-center justify-center`} style={{ height: '24rem' }}>
<Spinner size={'base'}/>
</div>
<AdminContentBlock showFlashKey={'egg'}>
<Spinner size={'large'} centered/>
</AdminContentBlock>
);
}
@ -62,15 +69,15 @@ const EggRouter = () => {
<Switch location={location}>
<Route path={`${match.path}`} exact>
<EggSettingsContainer egg={egg}/>
<EggSettingsContainer/>
</Route>
<Route path={`${match.path}/variables`} exact>
<EggVariablesContainer egg={egg}/>
<EggVariablesContainer/>
</Route>
<Route path={`${match.path}/install`} exact>
<EggInstallContainer egg={egg}/>
<EggInstallContainer/>
</Route>
</Switch>
</AdminContentBlock>

View file

@ -1,3 +1,4 @@
import { useEggFromRoute } from '@/api/admin/egg';
import updateEgg from '@/api/admin/eggs/updateEgg';
import EggDeleteButton from '@/components/admin/nests/eggs/EggDeleteButton';
import EggExportButton from '@/components/admin/nests/eggs/EggExportButton';
@ -13,7 +14,6 @@ import { faDocker } from '@fortawesome/free-brands-svg-icons';
import { faEgg, faFireAlt, faMicrochip, faTerminal } from '@fortawesome/free-solid-svg-icons';
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import AdminBox from '@/components/admin/AdminBox';
import { Egg } from '@/api/admin/eggs/getEgg';
import { useHistory } from 'react-router-dom';
import tw from 'twin.macro';
import { object } from 'yup';
@ -45,7 +45,13 @@ export function EggInformationContainer () {
);
}
function EggDetailsContainer ({ egg }: { egg: Egg }) {
function EggDetailsContainer () {
const { data: egg } = useEggFromRoute();
if (!egg) {
return null;
}
return (
<AdminBox icon={faEgg} title={'Egg Details'} css={tw`relative`}>
<div css={tw`mb-6`}>
@ -200,12 +206,18 @@ interface Values {
configFiles: string;
}
export default function EggSettingsContainer ({ egg }: { egg: Egg }) {
export default function EggSettingsContainer () {
const history = useHistory();
const ref = useRef<EggProcessContainerRef>();
const { clearFlashes, clearAndAddHttpError } = useFlash();
const ref = useRef<EggProcessContainerRef>();
const { data: egg } = useEggFromRoute();
if (!egg) {
return null;
}
const submit = async (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('egg');
@ -240,7 +252,7 @@ export default function EggSettingsContainer ({ egg }: { egg: Egg }) {
<Form>
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6`}>
<EggInformationContainer/>
<EggDetailsContainer egg={egg}/>
<EggDetailsContainer/>
</div>
<EggStartupContainer css={tw`mb-6`}/>

View file

@ -5,7 +5,7 @@ import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import React, { useState } from 'react';
import tw from 'twin.macro';
import { array, boolean, object, string } from 'yup';
import getEgg, { Egg, EggVariable } from '@/api/admin/eggs/getEgg';
import { EggVariable, useEggFromRoute } from '@/api/admin/egg';
import updateEggVariables from '@/api/admin/eggs/updateEggVariables';
import NewVariableButton from '@/components/admin/nests/eggs/NewVariableButton';
import AdminBox from '@/components/admin/AdminBox';
@ -19,10 +19,10 @@ import { TrashIcon } from '@heroicons/react/outline';
export const validationSchema = object().shape({
name: string().required().min(1).max(191),
description: string(),
envVariable: string().required().min(1).max(191),
environmentVariable: string().required().min(1).max(191),
defaultValue: string(),
userViewable: boolean().required(),
userEditable: boolean().required(),
isUserViewable: boolean().required(),
isUserEditable: boolean().required(),
rules: string().required(),
});
@ -47,8 +47,8 @@ export function EggVariableForm ({ prefix }: { prefix: string }) {
<FieldRow>
<Field
id={`${prefix}envVariable`}
name={`${prefix}envVariable`}
id={`${prefix}environmentVariable`}
name={`${prefix}environmentVariable`}
label={'Environment Variable'}
type={'text'}
/>
@ -63,14 +63,14 @@ export function EggVariableForm ({ prefix }: { prefix: string }) {
<div css={tw`flex flex-row mb-6`}>
<Checkbox
id={`${prefix}userViewable`}
name={`${prefix}userViewable`}
id={`${prefix}isUserViewable`}
name={`${prefix}isUserViewable`}
label={'User Viewable'}
/>
<Checkbox
id={`${prefix}userEditable`}
name={`${prefix}userEditable`}
id={`${prefix}isUserEditable`}
name={`${prefix}isUserEditable`}
label={'User Editable'}
css={tw`ml-auto`}
/>
@ -95,7 +95,7 @@ function EggVariableDeleteButton ({ onClick }: { onClick: (success: () => void)
setLoading(true);
onClick(() => {
setLoading(false);
//setLoading(false);
});
};
@ -140,14 +140,18 @@ function EggVariableBox ({ onDeleteClick, variable, prefix }: { onDeleteClick: (
);
}
export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
export default function EggVariablesContainer () {
const { clearAndAddHttpError } = useFlash();
const { mutate } = getEgg(egg.id);
const { data: egg, mutate } = useEggFromRoute();
if (!egg) {
return null;
}
const submit = (values: EggVariable[], { setSubmitting }: FormikHelpers<EggVariable[]>) => {
updateEggVariables(egg.id, values)
.then(async (variables) => await mutate(egg => ({ ...egg!, relations: { variables: variables } })))
.then(async () => await mutate())
.catch(error => clearAndAddHttpError({ key: 'egg', error }))
.then(() => setSubmitting(false));
};
@ -155,17 +159,17 @@ export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
return (
<Formik
onSubmit={submit}
initialValues={egg.relations?.variables || []}
initialValues={egg.relationships.variables}
validationSchema={array().of(validationSchema)}
>
{({ isSubmitting, isValid }) => (
<Form>
<div css={tw`flex flex-col mb-16`}>
{egg.relations?.variables?.length === 0 ?
{egg.relationships.variables?.length === 0 ?
<NoItems css={tw`bg-neutral-700 rounded-md shadow-md`}/>
:
<div css={tw`grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-6`}>
{egg.relations?.variables?.map((v, i) => (
<div css={tw`grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-6`}>
{egg.relationships.variables.map((v, i) => (
<EggVariableBox
key={i}
prefix={`[${i}].`}
@ -175,8 +179,9 @@ export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
.then(async () => {
await mutate(egg => ({
...egg!,
relations: {
variables: egg!.relations.variables!.filter(v2 => v.id === v2.id),
relationships: {
...egg!.relationships,
variables: egg!.relationships.variables!.filter(v2 => v.id === v2.id),
},
}));
success();
@ -190,7 +195,7 @@ export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
<div css={tw`bg-neutral-700 rounded shadow-md py-2 px-4 mt-6`}>
<div css={tw`flex flex-row`}>
<NewVariableButton eggId={egg.id}/>
<NewVariableButton/>
<Button type={'submit'} size={'small'} css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
Save Changes

View file

@ -1,26 +1,32 @@
import { Form, Formik, FormikHelpers } from 'formik';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import React, { useState } from 'react';
import tw from 'twin.macro';
import createEggVariable from '@/api/admin/eggs/createEggVariable';
import getEgg from '@/api/admin/eggs/getEgg';
import createEggVariable, { CreateEggVariable } from '@/api/admin/eggs/createEggVariable';
import { useEggFromRoute } from '@/api/admin/egg';
import { EggVariableForm, validationSchema } from '@/components/admin/nests/eggs/EggVariablesContainer';
import Modal from '@/components/elements/Modal';
import FlashMessageRender from '@/components/FlashMessageRender';
import Button from '@/components/elements/Button';
import useFlash from '@/plugins/useFlash';
export default function NewVariableButton ({ eggId }: { eggId: number }) {
export default function NewVariableButton () {
const { setValues } = useFormikContext();
const [ visible, setVisible ] = useState(false);
const { clearFlashes, clearAndAddHttpError } = useFlash();
const { mutate } = getEgg(eggId);
const { data: egg, mutate } = useEggFromRoute();
const submit = (values: any, { setSubmitting }: FormikHelpers<any>) => {
if (!egg) {
return null;
}
const submit = (values: CreateEggVariable, { setSubmitting }: FormikHelpers<CreateEggVariable>) => {
clearFlashes('variable:create');
createEggVariable(eggId, values)
.then(async (v) => {
await mutate(egg => ({ ...egg!, relations: { variables: [ ...egg!.relations.variables!, v ] } }));
createEggVariable(egg.id, values)
.then(async (variable) => {
setValues([ ...egg.relationships.variables, variable ]);
await mutate(egg => ({ ...egg!, relationships: { ...egg!.relationships, variables: [ ...egg!.relationships.variables, variable ] } }));
setVisible(false);
})
.catch(error => {
@ -36,15 +42,15 @@ export default function NewVariableButton ({ eggId }: { eggId: number }) {
initialValues={{
name: '',
description: '',
envVariable: '',
environmentVariable: '',
defaultValue: '',
userViewable: false,
userEditable: false,
isUserViewable: false,
isUserEditable: false,
rules: '',
}}
validationSchema={validationSchema}
>
{({ isSubmitting, resetForm }) => (
{({ isSubmitting, isValid, resetForm }) => (
<Modal
visible={visible}
dismissable={!isSubmitting}
@ -70,7 +76,7 @@ export default function NewVariableButton ({ eggId }: { eggId: number }) {
>
Cancel
</Button>
<Button css={tw`w-full mt-4 sm:w-auto sm:mt-0`} type={'submit'}>
<Button css={tw`w-full mt-4 sm:w-auto sm:mt-0`} type={'submit'} disabled={isSubmitting || !isValid}>
Create Variable
</Button>
</div>

View file

@ -146,7 +146,6 @@ export default () => {
const { clearFlashes, clearAndAddHttpError } = useFlash();
const submit = (r: CreateServerRequest, { setSubmitting }: FormikHelpers<CreateServerRequest>) => {
console.log(r);
clearFlashes('server:create');
createServer(r)

View file

@ -32,7 +32,7 @@ export default () => {
if (!server || (error && isValidating)) {
return (
<AdminContentBlock showFlashKey={'server'}>
<Spinner size={'large'} centered/>;
<Spinner size={'large'} centered/>
</AdminContentBlock>
);
}