ui(admin): add new egg variable option

This commit is contained in:
Matthew Penner 2021-10-03 15:51:35 -06:00
parent 1eed25dcc7
commit b2aa05dc07
No known key found for this signature in database
GPG key ID: BAB67850901908A8
8 changed files with 168 additions and 102 deletions

View file

@ -4,7 +4,7 @@ import { EggVariable, rawDataToEggVariable } from '@/api/admin/eggs/getEgg';
export default (eggId: number, variable: Omit<EggVariable, 'id' | 'eggId' | 'createdAt' | 'updatedAt'>): Promise<EggVariable> => { export default (eggId: number, variable: Omit<EggVariable, 'id' | 'eggId' | 'createdAt' | 'updatedAt'>): Promise<EggVariable> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post( http.post(
`/api/application/eggs/${eggId}`, `/api/application/eggs/${eggId}/variables`,
{ {
name: variable.name, name: variable.name,
description: variable.description, description: variable.description,

View file

@ -1,6 +1,7 @@
import { Nest } from '@/api/admin/nests/getNests'; import { Nest } from '@/api/admin/nests/getNests';
import { rawDataToServer, Server } from '@/api/admin/servers/getServers'; import { rawDataToServer, Server } from '@/api/admin/servers/getServers';
import http, { FractalResponseData, FractalResponseList } from '@/api/http'; import http, { FractalResponseData, FractalResponseList } from '@/api/http';
import useSWR from 'swr';
export interface EggVariable { export interface EggVariable {
id: number; id: number;
@ -88,10 +89,16 @@ export const rawDataToEgg = ({ attributes }: FractalResponseData): Egg => ({
}, },
}); });
export default (id: number, include: string[] = []): Promise<Egg> => { export const getEgg = async (id: number): Promise<Egg> => {
return new Promise((resolve, reject) => { const { data } = await http.get(`/api/application/eggs/${id}`, { params: { include: [ 'variables' ] } });
http.get(`/api/application/eggs/${id}`, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToEgg(data))) return rawDataToEgg(data);
.catch(reject); };
export default (id: number) => {
return useSWR<Egg>(`egg:${id}`, async () => {
const { data } = await http.get(`/api/application/eggs/${id}`, { params: { include: [ 'variables' ] } });
return rawDataToEgg(data);
}); });
}; };

View file

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

View file

@ -1,54 +1,23 @@
import EggInstallContainer from '@/components/admin/nests/eggs/EggInstallContainer'; import EggInstallContainer from '@/components/admin/nests/eggs/EggInstallContainer';
import EggVariablesContainer from '@/components/admin/nests/eggs/EggVariablesContainer'; import EggVariablesContainer from '@/components/admin/nests/eggs/EggVariablesContainer';
import React, { useEffect, useState } from 'react'; import React from 'react';
import { useLocation } from 'react-router'; import { useLocation } from 'react-router';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { Route, Switch, useRouteMatch } from 'react-router-dom'; import { Route, Switch, useRouteMatch } from 'react-router-dom';
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy'; import getEgg from '@/api/admin/eggs/getEgg';
import getEgg, { Egg } from '@/api/admin/eggs/getEgg';
import AdminContentBlock from '@/components/admin/AdminContentBlock'; import AdminContentBlock from '@/components/admin/AdminContentBlock';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import { ApplicationStore } from '@/state';
import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation'; import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation';
import EggSettingsContainer from '@/components/admin/nests/eggs/EggSettingsContainer'; import EggSettingsContainer from '@/components/admin/nests/eggs/EggSettingsContainer';
interface ctx {
egg: Egg | undefined;
setEgg: Action<ctx, Egg | undefined>;
}
export const Context = createContextStore<ctx>({
egg: undefined,
setEgg: action((state, payload) => {
state.egg = payload;
}),
});
const EggRouter = () => { const EggRouter = () => {
const location = useLocation(); const location = useLocation();
const match = useRouteMatch<{ id?: string }>(); const match = useRouteMatch<{ id?: string }>();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { data: egg } = getEgg(Number(match.params?.id));
const [ loading, setLoading ] = useState(true);
const egg = Context.useStoreState(state => state.egg); if (egg === undefined) {
const setEgg = Context.useStoreActions(actions => actions.setEgg);
useEffect(() => {
clearFlashes('egg');
getEgg(Number(match.params?.id), [ 'variables' ])
.then(egg => setEgg(egg))
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'egg', error });
})
.then(() => setLoading(false));
}, []);
if (loading || egg === undefined) {
return ( return (
<AdminContentBlock> <AdminContentBlock>
<FlashMessageRender byKey={'egg'} css={tw`mb-4`}/> <FlashMessageRender byKey={'egg'} css={tw`mb-4`}/>
@ -109,9 +78,5 @@ const EggRouter = () => {
}; };
export default () => { export default () => {
return ( return <EggRouter/>;
<Context.Provider>
<EggRouter/>
</Context.Provider>
);
}; };

View file

@ -162,7 +162,7 @@ const EggProcessContainer = forwardRef<any, EggProcessContainerProps>(
<AdminBox icon={faMicrochip} title={'Process Configuration'} css={tw`relative`} className={className}> <AdminBox icon={faMicrochip} title={'Process Configuration'} css={tw`relative`} className={className}>
<SpinnerOverlay visible={isSubmitting}/> <SpinnerOverlay visible={isSubmitting}/>
<div css={tw`mb-6`}> <div css={tw`mb-5`}>
<Label>Startup Configuration</Label> <Label>Startup Configuration</Label>
<Editor <Editor
mode={jsonLanguage} mode={jsonLanguage}

View file

@ -1,9 +1,10 @@
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import React, { useEffect } from 'react'; import React from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { array, boolean, object, string } from 'yup'; import { array, boolean, object, string } from 'yup';
import { Egg, EggVariable } from '@/api/admin/eggs/getEgg'; import getEgg, { Egg, EggVariable } from '@/api/admin/eggs/getEgg';
import updateEggVariables from '@/api/admin/eggs/updateEggVariables'; import updateEggVariables from '@/api/admin/eggs/updateEggVariables';
import NewVariableButton from '@/components/admin/nests/eggs/NewVariableButton';
import AdminBox from '@/components/admin/AdminBox'; import AdminBox from '@/components/admin/AdminBox';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import Checkbox from '@/components/elements/Checkbox'; import Checkbox from '@/components/elements/Checkbox';
@ -11,91 +12,7 @@ import Field, { FieldRow, TextareaField } from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
function EggVariableForm ({ variable: { name }, i }: { variable: EggVariable, i: number }) { export const validationSchema = object().shape({
const { isSubmitting } = useFormikContext();
return (
<AdminBox css={tw`relative w-full`} title={<p css={tw`text-sm uppercase`}>{name}</p>}>
<SpinnerOverlay visible={isSubmitting}/>
<Field
id={`[${i}].name`}
name={`[${i}].name`}
label={'Name'}
type={'text'}
css={tw`mb-6`}
/>
<TextareaField
id={`[${i}].description`}
name={`[${i}].description`}
label={'Description'}
rows={3}
css={tw`mb-4`}
/>
<FieldRow>
<Field
id={`[${i}].envVariable`}
name={`[${i}].envVariable`}
label={'Environment Variable'}
type={'text'}
/>
<Field
id={`[${i}].defaultValue`}
name={`[${i}].defaultValue`}
label={'Default Value'}
type={'text'}
/>
</FieldRow>
<div css={tw`flex flex-row mb-6`}>
<Checkbox
id={`[${i}].userViewable`}
name={`[${i}].userViewable`}
label={'User Viewable'}
/>
<Checkbox
id={`[${i}].userEditable`}
name={`[${i}].userEditable`}
label={'User Editable'}
css={tw`ml-auto`}
/>
</div>
<Field
id={`[${i}].rules`}
name={`[${i}].rules`}
label={'Validation Rules'}
type={'text'}
css={tw`mb-2`}
/>
</AdminBox>
);
}
export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
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={egg.relations?.variables || []}
validationSchema={array().of(
object().shape({
name: string().required().min(1).max(191), name: string().required().min(1).max(191),
description: string(), description: string(),
envVariable: string().required().min(1).max(191), envVariable: string().required().min(1).max(191),
@ -103,19 +20,111 @@ export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
userViewable: boolean().required(), userViewable: boolean().required(),
userEditable: boolean().required(), userEditable: boolean().required(),
rules: string().required(), rules: string().required(),
}), });
)}
export function EggVariableForm ({ prefix }: { prefix: string }) {
return (
<>
<Field
id={`${prefix}name`}
name={`${prefix}name`}
label={'Name'}
type={'text'}
css={tw`mb-6`}
/>
<TextareaField
id={`${prefix}description`}
name={`${prefix}description`}
label={'Description'}
rows={3}
css={tw`mb-4`}
/>
<FieldRow>
<Field
id={`${prefix}envVariable`}
name={`${prefix}envVariable`}
label={'Environment Variable'}
type={'text'}
/>
<Field
id={`${prefix}defaultValue`}
name={`${prefix}defaultValue`}
label={'Default Value'}
type={'text'}
/>
</FieldRow>
<div css={tw`flex flex-row mb-6`}>
<Checkbox
id={`${prefix}userViewable`}
name={`${prefix}userViewable`}
label={'User Viewable'}
/>
<Checkbox
id={`${prefix}userEditable`}
name={`${prefix}userEditable`}
label={'User Editable'}
css={tw`ml-auto`}
/>
</div>
<Field
id={`${prefix}rules`}
name={`${prefix}rules`}
label={'Validation Rules'}
type={'text'}
css={tw`mb-2`}
/>
</>
);
}
function EggVariableBox ({ variable, prefix }: { variable: EggVariable, prefix: string }) {
const { isSubmitting } = useFormikContext();
return (
<AdminBox css={tw`relative w-full`} title={<p css={tw`text-sm uppercase`}>{variable.name}</p>}>
<SpinnerOverlay visible={isSubmitting}/>
<EggVariableForm prefix={prefix}/>
</AdminBox>
);
}
export default function EggVariablesContainer ({ egg }: { egg: Egg }) {
const { clearAndAddHttpError } = useFlash();
const { mutate } = getEgg(egg.id);
const submit = (values: EggVariable[], { setSubmitting }: FormikHelpers<EggVariable[]>) => {
updateEggVariables(egg.id, values)
.then(async (variables) => await mutate(egg => ({ ...egg!, relations: { variables: variables } })))
.catch(error => clearAndAddHttpError({ key: 'egg', error }))
.then(() => setSubmitting(false));
};
return (
<Formik
onSubmit={submit}
initialValues={egg.relations?.variables || []}
validationSchema={array().of(validationSchema)}
> >
{({ 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`}> <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}/>)} {egg.relations?.variables?.map((v, i) => <EggVariableBox key={i} prefix={`[${i}].`} variable={v}/>)}
</div> </div>
<div css={tw`bg-neutral-700 rounded shadow-md py-2 pr-6 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`}>
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}> <NewVariableButton eggId={egg.id}/>
<Button type={'submit'} size={'small'} css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
Save Changes Save Changes
</Button> </Button>
</div> </div>

View file

@ -0,0 +1,87 @@
import { Form, Formik, FormikHelpers } 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 { 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 }) {
const [ visible, setVisible ] = useState(false);
const { clearFlashes, clearAndAddHttpError } = useFlash();
const { mutate } = getEgg(eggId);
const submit = (values: any, { setSubmitting }: FormikHelpers<any>) => {
clearFlashes('variable:create');
createEggVariable(eggId, values)
.then(async (v) => {
await mutate(egg => ({ ...egg!, relations: { variables: [ ...egg!.relations.variables!, v ] } }));
setVisible(false);
})
.catch(error => {
clearAndAddHttpError({ key: 'variable:create', error });
setSubmitting(false);
});
};
return (
<>
<Formik
onSubmit={submit}
initialValues={{
name: '',
description: '',
envVariable: '',
defaultValue: '',
userViewable: false,
userEditable: false,
rules: '',
}}
validationSchema={validationSchema}
>
{({ isSubmitting, resetForm }) => (
<Modal
visible={visible}
dismissable={!isSubmitting}
showSpinnerOverlay={isSubmitting}
onDismissed={() => {
resetForm();
setVisible(false);
}}
>
<FlashMessageRender byKey={'variable:create'} css={tw`mb-6`}/>
<h2 css={tw`mb-6 text-2xl text-neutral-100`}>New Variable</h2>
<Form css={tw`m-0`}>
<EggVariableForm prefix={''}/>
<div css={tw`flex flex-wrap justify-end mt-6`}>
<Button
type={'button'}
isSecondary
css={tw`w-full sm:w-auto sm:mr-2`}
onClick={() => setVisible(false)}
>
Cancel
</Button>
<Button css={tw`w-full mt-4 sm:w-auto sm:mt-0`} type={'submit'}>
Create Variable
</Button>
</div>
</Form>
</Modal>
)}
</Formik>
<Button type={'button'} color={'green'} onClick={() => setVisible(true)}>
Add a fucking variable
</Button>
</>
);
}

View file

@ -1,4 +1,4 @@
import getEgg, { Egg, EggVariable } from '@/api/admin/eggs/getEgg'; import { getEgg, Egg, EggVariable } from '@/api/admin/eggs/getEgg';
import { Server } from '@/api/admin/servers/getServers'; import { Server } from '@/api/admin/servers/getServers';
import updateServerStartup, { Values } from '@/api/admin/servers/updateServerStartup'; import updateServerStartup, { Values } from '@/api/admin/servers/updateServerStartup';
import EggSelect from '@/components/admin/servers/EggSelect'; import EggSelect from '@/components/admin/servers/EggSelect';
@ -187,7 +187,7 @@ export default function ServerStartupContainer ({ server }: { server: Server })
const setServer = Context.useStoreActions(actions => actions.setServer); const setServer = Context.useStoreActions(actions => actions.setServer);
useEffect(() => { useEffect(() => {
getEgg(server.eggId, [ 'variables' ]) getEgg(server.eggId)
.then(egg => setEgg(egg)) .then(egg => setEgg(egg))
.catch(error => console.error(error)); .catch(error => console.error(error));
}, []); }, []);