From 04649d20b3fe12ea0eff4e489686a32e52312cec Mon Sep 17 00:00:00 2001 From: nils Date: Wed, 29 Jan 2025 18:30:59 +0100 Subject: [PATCH] add the new instance selector to the service form --- .../instance-selector/region-selector.tsx | 2 - .../06-instance/instance-selector.new.tsx | 96 +++++++++++++ .../06-instance/instance-selector.tsx | 85 ++++++++++++ .../sections/06-instance/instance.section.tsx | 126 ++++++------------ src/utils/factories.ts | 1 + 5 files changed, 226 insertions(+), 84 deletions(-) create mode 100644 src/modules/service-form/sections/06-instance/instance-selector.new.tsx create mode 100644 src/modules/service-form/sections/06-instance/instance-selector.tsx diff --git a/src/components/instance-selector/region-selector.tsx b/src/components/instance-selector/region-selector.tsx index 3c28bf8d..e350aa31 100644 --- a/src/components/instance-selector/region-selector.tsx +++ b/src/components/instance-selector/region-selector.tsx @@ -1,5 +1,3 @@ -import clsx from 'clsx'; - import { CheckboxInput, Collapse, TabButton, TabButtons } from '@koyeb/design-system'; import { CatalogRegion, RegionScope } from 'src/api/model'; import { useRegionLatency } from 'src/hooks/region-latency'; diff --git a/src/modules/service-form/sections/06-instance/instance-selector.new.tsx b/src/modules/service-form/sections/06-instance/instance-selector.new.tsx new file mode 100644 index 00000000..0ddf2824 --- /dev/null +++ b/src/modules/service-form/sections/06-instance/instance-selector.new.tsx @@ -0,0 +1,96 @@ +import { useState } from 'react'; +import { useController, useFormContext } from 'react-hook-form'; + +import { useInstance, useInstances, useRegions } from 'src/api/hooks/catalog'; +import { CatalogInstance, CatalogRegion, InstanceCategory } from 'src/api/model'; +import { useIsInstanceAvailable, useIsRegionAvailable } from 'src/application/instance-region-availability'; +import { useGetInstanceBadges } from 'src/components/instance-selector/instance-badges'; +import { InstanceCategoryTabs } from 'src/components/instance-selector/instance-category-tabs'; +import { InstanceSelector as InstanceSelectorComponent } from 'src/components/instance-selector/instance-selector'; +import { hasProperty } from 'src/utils/object'; + +import { ServiceForm } from '../../service-form.types'; +import { useWatchServiceForm } from '../../use-service-form'; + +import { InstanceAlerts } from './instance-alerts'; + +export function InstanceSelectorNew() { + const instances = useInstances(); + const regions = useRegions(); + + const serviceType = useWatchServiceForm('serviceType'); + const hasVolumes = useWatchServiceForm('volumes').filter((volume) => volume.name !== '').length > 0; + const previousInstance = useInstance(useWatchServiceForm('meta.previousInstance')); + + const selectedRegions = useWatchServiceForm('regions').map( + (identifier) => regions.find(hasProperty('identifier', identifier))!, + ); + + const { getValues, setValue, trigger } = useFormContext(); + + const { field } = useController({ name: 'instance' }); + const instance = useInstance(field.value); + + const [category, setCategory] = useState(instance?.category ?? 'standard'); + + const isInstanceAvailable = useIsInstanceAvailable({ serviceType, hasVolumes, previousInstance }); + const isRegionAvailable = useIsRegionAvailable({ instance }); + + const handleInstanceSelected = (instance: CatalogInstance | null) => { + if (!instance || !isInstanceAvailable(instance)) { + field.onChange(null); + return; + } + + field.onChange(instance.identifier); + + if (instance.category === 'eco') { + setValue('scaling.max', getValues('scaling.min')); + void trigger('scaling'); + } + + if (instance.identifier === 'free') { + setValue('scaling.min', 1); + setValue('scaling.max', 1); + void trigger('scaling'); + } + }; + + const handleRegionsSelected = (regions: CatalogRegion[]) => { + setValue( + 'regions', + regions.map((region) => region.identifier), + { shouldValidate: true }, + ); + }; + + const getBadges = useGetInstanceBadges({ + previousInstance: useInstance(useFormContext().watch('meta.previousInstance')), + }); + + return ( + <> + + + + + + + ); +} diff --git a/src/modules/service-form/sections/06-instance/instance-selector.tsx b/src/modules/service-form/sections/06-instance/instance-selector.tsx new file mode 100644 index 00000000..bef9e0d9 --- /dev/null +++ b/src/modules/service-form/sections/06-instance/instance-selector.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react'; +import { useController, useFormContext } from 'react-hook-form'; + +import { useInstance, useInstances, useRegion } from 'src/api/hooks/catalog'; +import { CatalogInstance, InstanceCategory } from 'src/api/model'; +import { + useInstanceAvailabilities, + useRegionAvailabilities, +} from 'src/application/instance-region-availability'; +import { InstanceSelector as InstanceSelectorComponent } from 'src/components/instance-selector'; +import { hasProperty } from 'src/utils/object'; + +import { ServiceForm } from '../../service-form.types'; +import { useWatchServiceForm } from '../../use-service-form'; + +import { InstanceAlerts } from './instance-alerts'; + +export function InstanceSelector() { + const instances = useInstances(); + + const serviceType = useWatchServiceForm('serviceType'); + const hasVolumes = useWatchServiceForm('volumes').filter((volume) => volume.name !== '').length > 0; + const previousInstance = useInstance(useWatchServiceForm('meta.previousInstance')); + + const regions = useWatchServiceForm('regions'); + const firstRegion = useRegion(regions[0]); + + const instanceAvailabilities = useInstanceAvailabilities({ serviceType, hasVolumes, previousInstance }); + const regionAvailabilities = useRegionAvailabilities(); + + const { getValues, setValue, trigger } = useFormContext(); + + const { field } = useController({ name: 'instance' }); + const instance = useInstance(field.value); + + const [category, setCategory] = useState(instance?.category ?? 'standard'); + + const handleInstanceSelected = (instance: CatalogInstance | null) => { + const [isAvailable] = instance ? (instanceAvailabilities[instance.identifier] ?? [false]) : [false]; + + if (!instance || !isAvailable) { + field.onChange(null); + return; + } + + field.onChange(instance.identifier); + + if (instance.category === 'eco') { + setValue('scaling.max', getValues('scaling.min')); + void trigger('scaling'); + } + + if (instance.identifier === 'free') { + setValue('scaling.min', 1); + setValue('scaling.max', 1); + void trigger('scaling'); + } + + let availableRegions = getValues('regions') + .filter((region) => regionAvailabilities[region]?.[0]) + .filter((region) => instance.regions?.includes(region)); + + if (availableRegions.length === 0) { + availableRegions = [instance?.regions?.[0] ?? 'fra']; + } + + setValue('regions', availableRegions, { shouldValidate: true }); + }; + + return ( + <> + + + instanceAvailabilities[instance] ?? [false, 'instanceNotFound']} + selectedInstance={instances.find(hasProperty('identifier', field.value)) ?? null} + previousInstance={previousInstance} + onInstanceSelected={handleInstanceSelected} + onCategoryChanged={setCategory} + className="w-full" + /> + + ); +} diff --git a/src/modules/service-form/sections/06-instance/instance.section.tsx b/src/modules/service-form/sections/06-instance/instance.section.tsx index 5188f301..02ec6a88 100644 --- a/src/modules/service-form/sections/06-instance/instance.section.tsx +++ b/src/modules/service-form/sections/06-instance/instance.section.tsx @@ -1,77 +1,21 @@ -import { useState } from 'react'; -import { useController, useFormContext } from 'react-hook-form'; +import { useFormContext } from 'react-hook-form'; import { Badge } from '@koyeb/design-system'; -import { useInstance, useInstances, useRegion } from 'src/api/hooks/catalog'; -import { CatalogInstance, InstanceCategory } from 'src/api/model'; -import { - useInstanceAvailabilities, - useRegionAvailabilities, -} from 'src/application/instance-region-availability'; -import { InstanceSelector } from 'src/components/instance-selector'; +import { useInstance, useRegions } from 'src/api/hooks/catalog'; +import { RegionFlag } from 'src/components/region-flag'; +import { RegionName } from 'src/components/region-name'; +import { FeatureFlag } from 'src/hooks/feature-flag'; import { createTranslate, Translate } from 'src/intl/translate'; -import { hasProperty } from 'src/utils/object'; import { ServiceFormSection } from '../../components/service-form-section'; import { ServiceForm } from '../../service-form.types'; -import { useWatchServiceForm } from '../../use-service-form'; -import { InstanceAlerts } from './instance-alerts'; +import { InstanceSelector } from './instance-selector'; +import { InstanceSelectorNew } from './instance-selector.new'; const T = createTranslate('modules.serviceForm.instance'); export function InstanceSection() { - const instances = useInstances(); - - const serviceType = useWatchServiceForm('serviceType'); - const hasVolumes = useWatchServiceForm('volumes').filter((volume) => volume.name !== '').length > 0; - const previousInstance = useInstance(useWatchServiceForm('meta.previousInstance')); - - const regions = useWatchServiceForm('regions'); - const firstRegion = useRegion(regions[0]); - - const instanceAvailabilities = useInstanceAvailabilities({ serviceType, hasVolumes, previousInstance }); - const regionAvailabilities = useRegionAvailabilities(); - - const { getValues, setValue, trigger } = useFormContext(); - - const { field } = useController({ name: 'instance' }); - const instance = useInstance(field.value); - - const [category, setCategory] = useState(instance?.category ?? 'standard'); - - const handleInstanceSelected = (instance: CatalogInstance | null) => { - const [isAvailable] = instance ? (instanceAvailabilities[instance.identifier] ?? [false]) : [false]; - - if (!instance || !isAvailable) { - field.onChange(null); - return; - } - - field.onChange(instance.identifier); - - if (instance.category === 'eco') { - setValue('scaling.max', getValues('scaling.min')); - void trigger('scaling'); - } - - if (instance.identifier === 'free') { - setValue('scaling.min', 1); - setValue('scaling.max', 1); - void trigger('scaling'); - } - - let availableRegions = getValues('regions') - .filter((region) => regionAvailabilities[region]?.[0]) - .filter((region) => instance.regions?.includes(region)); - - if (availableRegions.length === 0) { - availableRegions = [instance?.regions?.[0] ?? 'fra']; - } - - setValue('regions', availableRegions, { shouldValidate: true }); - }; - return ( } className="col gap-6" > - - - instanceAvailabilities[instance] ?? [false, 'instanceNotFound']} - selectedInstance={instances.find(hasProperty('identifier', field.value)) ?? null} - previousInstance={previousInstance} - onInstanceSelected={handleInstanceSelected} - onCategoryChanged={setCategory} - className="w-full" - /> + }> + + ); } function SectionTitle() { - const instance = useInstance(useWatchServiceForm('instance')); + const instance = useInstance(useFormContext().watch('instance')); + const regions = useRegions(useFormContext().watch('regions')); if (!instance) { return ; @@ -110,15 +47,40 @@ function SectionTitle() { ); return ( - <> - {instance.displayName} +
+ {instance.displayName} - + - - - - + + + + + + + {regions.length === 1 && ( + + + + + )} + + {regions.length > 1 && ( + + {regions.map((region, index) => ( + + ))} + + {regions.length} regions + + )} + +
); } diff --git a/src/utils/factories.ts b/src/utils/factories.ts index 375a9ad2..878cdaca 100644 --- a/src/utils/factories.ts +++ b/src/utils/factories.ts @@ -91,6 +91,7 @@ const region = createFactory(() => ({ datacenters: [], hasVolumes: false, category: 'koyeb', + scope: 'metropolitan', })); const instance = createFactory(() => ({