ui(admin): fix server egg select improperly switching

This commit is contained in:
Matthew Penner 2023-09-29 16:33:15 -06:00
parent 3721b2007b
commit 35ded9def8
No known key found for this signature in database
4 changed files with 67 additions and 58 deletions

View file

@ -56,7 +56,7 @@ export interface EggVariable extends Model {
* A standard API response with the minimum viable details for the frontend * A standard API response with the minimum viable details for the frontend
* to correctly render a egg. * to correctly render a egg.
*/ */
type LoadedEgg = WithRelationships<Egg, 'nest' | 'variables'>; export type LoadedEgg = WithRelationships<Egg, 'nest' | 'variables'>;
/** /**
* Gets a single egg from the database and returns it. * Gets a single egg from the database and returns it.

View file

@ -3,7 +3,7 @@ import type { ChangeEvent } from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { WithRelationships } from '@/api/admin'; import type { WithRelationships } from '@/api/admin';
import type { Egg } from '@/api/admin/egg'; import type {Egg, LoadedEgg} from '@/api/admin/egg';
import { searchEggs } from '@/api/admin/egg'; import { searchEggs } from '@/api/admin/egg';
import Label from '@/components/elements/Label'; import Label from '@/components/elements/Label';
import Select from '@/components/elements/Select'; import Select from '@/components/elements/Select';
@ -11,16 +11,16 @@ import Select from '@/components/elements/Select';
interface Props { interface Props {
nestId?: number; nestId?: number;
selectedEggId?: number; selectedEggId?: number;
onEggSelect: (egg: Egg | null) => void; onEggSelect: (egg: WithRelationships<Egg, 'variables'> | undefined) => void;
} }
export default ({ nestId, selectedEggId, onEggSelect }: Props) => { export default ({ nestId, selectedEggId, onEggSelect }: Props) => {
const [, , { setValue, setTouched }] = useField<Record<string, string | undefined>>('environment'); const [, , { setValue, setTouched }] = useField<Record<string, string | undefined>>('environment');
const [eggs, setEggs] = useState<WithRelationships<Egg, 'variables'>[] | null>(null); const [eggs, setEggs] = useState<WithRelationships<Egg, 'variables'>[] | undefined>(undefined);
const selectEgg = (egg: Egg | null) => { const selectEgg = (egg: WithRelationships<Egg, 'variables'> | undefined) => {
if (egg === null) { if (egg === undefined) {
onEggSelect(null); onEggSelect(undefined);
return; return;
} }
@ -40,26 +40,29 @@ export default ({ nestId, selectedEggId, onEggSelect }: Props) => {
useEffect(() => { useEffect(() => {
if (!nestId) { if (!nestId) {
setEggs(null); setEggs(undefined);
return; return;
} }
searchEggs(nestId, {}) searchEggs(nestId, {})
.then(eggs => { .then(_eggs => {
setEggs(eggs); setEggs(_eggs);
selectEgg(eggs[0] || null);
// If the currently selected egg is in the selected nest, use it instead of picking the first egg on the nest.
const egg = _eggs.find(egg => egg.id === selectedEggId) ?? _eggs[0];
selectEgg(egg);
}) })
.catch(error => console.error(error)); .catch(error => console.error(error));
}, [nestId]); }, [nestId]);
const onSelectChange = (e: ChangeEvent<HTMLSelectElement>) => { const onSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
selectEgg(eggs?.find(egg => egg.id.toString() === e.currentTarget.value) || null); selectEgg(eggs?.find(egg => egg.id.toString() === event.currentTarget.value) ?? undefined);
}; };
return ( return (
<> <>
<Label>Egg</Label> <Label>Egg</Label>
<Select id={'eggId'} name={'eggId'} defaultValue={selectedEggId} onChange={onSelectChange}> <Select id={'eggId'} name={'eggId'} value={selectedEggId} onChange={onSelectChange}>
{!eggs ? ( {!eggs ? (
<option disabled>Loading...</option> <option disabled>Loading...</option>
) : ( ) : (

View file

@ -30,6 +30,7 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import AdminContentBlock from '@/components/admin/AdminContentBlock'; import AdminContentBlock from '@/components/admin/AdminContentBlock';
import {WithRelationships} from "@/api/admin";
function InternalForm() { function InternalForm() {
const { const {
@ -39,12 +40,12 @@ function InternalForm() {
values: { environment }, values: { environment },
} = useFormikContext<CreateServerRequest>(); } = useFormikContext<CreateServerRequest>();
const [egg, setEgg] = useState<Egg | null>(null); const [egg, setEgg] = useState<WithRelationships<Egg, 'variables'> | undefined>(undefined);
const [node, setNode] = useState<Node | null>(null); const [node, setNode] = useState<Node | undefined>(undefined);
const [allocations, setAllocations] = useState<Allocation[] | null>(null); const [allocations, setAllocations] = useState<Allocation[] | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (egg === null) { if (egg === undefined) {
return; return;
} }
@ -54,7 +55,7 @@ function InternalForm() {
}, [egg]); }, [egg]);
useEffect(() => { useEffect(() => {
if (node === null) { if (node === undefined) {
return; return;
} }
@ -77,20 +78,20 @@ function InternalForm() {
</div> </div>
</BaseSettingsBox> </BaseSettingsBox>
<FeatureLimitsBox /> <FeatureLimitsBox />
<ServerServiceContainer egg={egg} setEgg={setEgg} nestId={0} /> <ServerServiceContainer selectedEggId={egg?.id} setEgg={setEgg} nestId={0} />
</div> </div>
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}> <div className="grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1">
<AdminBox icon={faNetworkWired} title={'Networking'} isLoading={isSubmitting}> <AdminBox icon={faNetworkWired} title="Networking" isLoading={isSubmitting}>
<div css={tw`grid grid-cols-1 gap-4 lg:gap-6`}> <div className="grid grid-cols-1 gap-4 lg:gap-6">
<div> <div>
<Label htmlFor={'allocation.default'}>Primary Allocation</Label> <Label htmlFor={'allocation.default'}>Primary Allocation</Label>
<Select <Select
id={'allocation.default'} id={'allocation.default'}
name={'allocation.default'} name={'allocation.default'}
disabled={node === null} disabled={node === undefined}
onChange={e => setFieldValue('allocation.default', Number(e.currentTarget.value))} onChange={e => setFieldValue('allocation.default', Number(e.currentTarget.value))}
> >
{node === null ? ( {node === undefined ? (
<option value="">Select a node...</option> <option value="">Select a node...</option>
) : ( ) : (
<option value="">Select an allocation...</option> <option value="">Select an allocation...</option>
@ -116,7 +117,7 @@ function InternalForm() {
<ServerImageContainer /> <ServerImageContainer />
</div> </div>
<AdminBox title={'Startup Command'} css={tw`relative w-full col-span-2`}> <AdminBox title={'Startup Command'} className="relative w-full col-span-2">
<SpinnerOverlay visible={isSubmitting} /> <SpinnerOverlay visible={isSubmitting} />
<Field <Field
@ -131,7 +132,7 @@ function InternalForm() {
/> />
</AdminBox> </AdminBox>
<div css={tw`col-span-2 grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}> <div className="col-span-2 grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8">
{/* This ensures that no variables are rendered unless the environment has a value for the variable. */} {/* This ensures that no variables are rendered unless the environment has a value for the variable. */}
{egg?.relationships.variables {egg?.relationships.variables
?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined) ?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined)
@ -140,9 +141,9 @@ function InternalForm() {
))} ))}
</div> </div>
<div css={tw`bg-neutral-700 rounded shadow-md px-4 py-3 col-span-2`}> <div className="bg-neutral-700 rounded shadow-md px-4 py-3 col-span-2">
<div css={tw`flex flex-row`}> <div className="flex flex-row">
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}> <Button type="submit" size="small" className="ml-auto" disabled={isSubmitting || !isValid}>
Create Server Create Server
</Button> </Button>
</div> </div>

View file

@ -7,7 +7,7 @@ import tw from 'twin.macro';
import { object } from 'yup'; import { object } from 'yup';
import type { InferModel } from '@/api/admin'; import type { InferModel } from '@/api/admin';
import type { Egg, EggVariable } from '@/api/admin/egg'; import type {Egg, EggVariable, LoadedEgg} from '@/api/admin/egg';
import { getEgg } from '@/api/admin/egg'; import { getEgg } from '@/api/admin/egg';
import type { Server } from '@/api/admin/server'; import type { Server } from '@/api/admin/server';
import { useServerFromRoute } from '@/api/admin/server'; import { useServerFromRoute } from '@/api/admin/server';
@ -23,12 +23,13 @@ import Field from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import Label from '@/components/elements/Label'; import Label from '@/components/elements/Label';
import type { ApplicationStore } from '@/state'; import type { ApplicationStore } from '@/state';
import {WithRelationships} from "@/api/admin";
function ServerStartupLineContainer({ egg, server }: { egg: Egg | null; server: Server }) { function ServerStartupLineContainer({ egg, server }: { egg?: Egg; server: Server }) {
const { isSubmitting, setFieldValue } = useFormikContext(); const { isSubmitting, setFieldValue } = useFormikContext();
useEffect(() => { useEffect(() => {
if (egg === null) { if (egg === undefined) {
return; return;
} }
@ -44,10 +45,10 @@ function ServerStartupLineContainer({ egg, server }: { egg: Egg | null; server:
}, [egg]); }, [egg]);
return ( return (
<AdminBox title={'Startup Command'} css={tw`relative w-full`}> <AdminBox title={'Startup Command'} className="relative w-full">
<SpinnerOverlay visible={isSubmitting} /> <SpinnerOverlay visible={isSubmitting} />
<div css={tw`mb-6`}> <div className="mb-6">
<Field <Field
id={'startup'} id={'startup'}
name={'startup'} name={'startup'}
@ -69,12 +70,12 @@ function ServerStartupLineContainer({ egg, server }: { egg: Egg | null; server:
} }
export function ServerServiceContainer({ export function ServerServiceContainer({
egg, selectedEggId,
setEgg, setEgg,
nestId: _nestId, nestId: _nestId,
}: { }: {
egg: Egg | null; selectedEggId?: number;
setEgg: (value: Egg | null) => void; setEgg: (value: WithRelationships<Egg, 'variables'> | undefined) => void;
nestId: number; nestId: number;
}) { }) {
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
@ -87,7 +88,7 @@ export function ServerServiceContainer({
<NestSelector selectedNestId={nestId} onNestSelect={setNestId} /> <NestSelector selectedNestId={nestId} onNestSelect={setNestId} />
</div> </div>
<div className="mb-6"> <div className="mb-6">
<EggSelect nestId={nestId} selectedEggId={egg?.id} onEggSelect={setEgg} /> <EggSelect nestId={nestId} selectedEggId={selectedEggId} onEggSelect={setEgg} />
</div> </div>
<div className="bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded"> <div className="bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded">
<FormikSwitch name={'skipScripts'} label={'Skip Egg Install Script'} description={'Soon™'} /> <FormikSwitch name={'skipScripts'} label={'Skip Egg Install Script'} description={'Soon™'} />
@ -100,10 +101,10 @@ export function ServerImageContainer() {
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
return ( return (
<AdminBox title={'Image Configuration'} css={tw`relative w-full`}> <AdminBox title={'Image Configuration'} className="relative w-full">
<SpinnerOverlay visible={isSubmitting} /> <SpinnerOverlay visible={isSubmitting} />
<div css={tw`md:w-full md:flex md:flex-col`}> <div className="md:w-full md:flex md:flex-col">
<div> <div>
{/* TODO: make this a proper select but allow a custom image to be specified if needed. */} {/* TODO: make this a proper select but allow a custom image to be specified if needed. */}
<Field id={'image'} name={'image'} label={'Docker Image'} type={'text'} /> <Field id={'image'} name={'image'} label={'Docker Image'} type={'text'} />
@ -130,7 +131,7 @@ export function ServerVariableContainer({ variable, value }: { variable: EggVari
}, [value]); }, [value]);
return ( return (
<AdminBox css={tw`relative w-full`} title={<p css={tw`text-sm uppercase`}>{variable.name}</p>}> <AdminBox className="relative w-full" title={<p className="text-sm uppercase">{variable.name}</p>}>
<SpinnerOverlay visible={isSubmitting} /> <SpinnerOverlay visible={isSubmitting} />
<Field <Field
@ -145,12 +146,14 @@ export function ServerVariableContainer({ variable, value }: { variable: EggVari
} }
function ServerStartupForm({ function ServerStartupForm({
selectedEggId,
egg, egg,
setEgg, setEgg,
server, server,
}: { }: {
egg: Egg | null; selectedEggId?: number;
setEgg: (value: Egg | null) => void; egg?: LoadedEgg;
setEgg: (value: LoadedEgg | undefined) => void;
server: Server; server: Server;
}) { }) {
const { const {
@ -161,22 +164,22 @@ function ServerStartupForm({
return ( return (
<Form> <Form>
<div css={tw`flex flex-col mb-16`}> <div className="flex flex-col mb-16">
<div css={tw`flex flex-row mb-6`}> <div className="flex flex-row mb-6">
<ServerStartupLineContainer egg={egg} server={server} /> <ServerStartupLineContainer egg={egg} server={server} />
</div> </div>
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6`}> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6">
<div css={tw`flex`}> <div className="flex">
<ServerServiceContainer egg={egg} setEgg={setEgg} nestId={server.nestId} /> <ServerServiceContainer selectedEggId={selectedEggId} setEgg={setEgg} nestId={server.nestId} />
</div> </div>
<div css={tw`flex`}> <div className="flex">
<ServerImageContainer /> <ServerImageContainer />
</div> </div>
</div> </div>
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}> <div className="grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8">
{/* This ensures that no variables are rendered unless the environment has a value for the variable. */} {/* This ensures that no variables are rendered unless the environment has a value for the variable. */}
{egg?.relationships.variables {egg?.relationships.variables
?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined) ?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined)
@ -193,9 +196,9 @@ function ServerStartupForm({
))} ))}
</div> </div>
<div css={tw`bg-neutral-700 rounded shadow-md py-2 pr-6 mt-6`}> <div className="bg-neutral-700 rounded shadow-md py-2 pr-6 mt-6">
<div css={tw`flex flex-row`}> <div className="flex flex-row">
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}> <Button type="submit" size="small" className="ml-auto" disabled={isSubmitting || !isValid}>
Save Changes Save Changes
</Button> </Button>
</div> </div>
@ -210,10 +213,12 @@ export default () => {
const { clearFlashes, clearAndAddHttpError } = useStoreActions( const { clearFlashes, clearAndAddHttpError } = useStoreActions(
(actions: Actions<ApplicationStore>) => actions.flashes, (actions: Actions<ApplicationStore>) => actions.flashes,
); );
const [egg, setEgg] = useState<InferModel<typeof getEgg> | null>(null); const [egg, setEgg] = useState<LoadedEgg | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (!server) return; if (!server) {
return;
}
getEgg(server.eggId) getEgg(server.eggId)
.then(egg => setEgg(egg)) .then(egg => setEgg(egg))
@ -249,10 +254,10 @@ export default () => {
validationSchema={object().shape({})} validationSchema={object().shape({})}
> >
<ServerStartupForm <ServerStartupForm
selectedEggId={egg?.id ?? server.eggId}
egg={egg} egg={egg}
// @ts-expect-error fix this
setEgg={setEgg} setEgg={setEgg}
server={server} server={server as Server}
/> />
</Formik> </Formik>
); );