ui(admin): fix server egg select improperly switching
This commit is contained in:
parent
3721b2007b
commit
35ded9def8
4 changed files with 67 additions and 58 deletions
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue