Skip to content

Commit

Permalink
add the new instance selector to the service form
Browse files Browse the repository at this point in the history
  • Loading branch information
nilscox committed Jan 29, 2025
1 parent c44869c commit 04649d2
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 84 deletions.
2 changes: 0 additions & 2 deletions src/components/instance-selector/region-selector.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ServiceForm>();

const { field } = useController<ServiceForm, 'instance'>({ name: 'instance' });
const instance = useInstance(field.value);

const [category, setCategory] = useState<InstanceCategory>(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<ServiceForm>().watch('meta.previousInstance')),
});

return (
<>
<InstanceCategoryTabs
category={category}
setCategory={setCategory}
instances={instances.filter(hasProperty('regionCategory', 'koyeb')).filter(isInstanceAvailable)}
setInstance={handleInstanceSelected}
/>

<InstanceAlerts selectedCategory={category} />

<InstanceSelectorComponent
instances={instances
.filter(hasProperty('regionCategory', 'koyeb'))
.filter(hasProperty('category', category))
.filter(isInstanceAvailable)}
selectedInstance={instances.find(hasProperty('identifier', field.value)) ?? null}
onInstanceSelected={handleInstanceSelected}
regions={regions.filter(isRegionAvailable)}
selectedRegions={selectedRegions}
onRegionsSelected={handleRegionsSelected}
getBadges={getBadges}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -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<ServiceForm>();

const { field } = useController<ServiceForm, 'instance'>({ name: 'instance' });
const instance = useInstance(field.value);

const [category, setCategory] = useState<InstanceCategory>(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 (
<>
<InstanceAlerts selectedCategory={category} />

<InstanceSelectorComponent
instances={instances.filter(hasProperty('regionCategory', firstRegion?.category ?? 'koyeb'))}
checkAvailability={(instance) => instanceAvailabilities[instance] ?? [false, 'instanceNotFound']}
selectedInstance={instances.find(hasProperty('identifier', field.value)) ?? null}
previousInstance={previousInstance}
onInstanceSelected={handleInstanceSelected}
onCategoryChanged={setCategory}
className="w-full"
/>
</>
);
}
126 changes: 44 additions & 82 deletions src/modules/service-form/sections/06-instance/instance.section.tsx
Original file line number Diff line number Diff line change
@@ -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<ServiceForm>();

const { field } = useController<ServiceForm, 'instance'>({ name: 'instance' });
const instance = useInstance(field.value);

const [category, setCategory] = useState<InstanceCategory>(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 (
<ServiceFormSection
section="instance"
Expand All @@ -80,23 +24,16 @@ export function InstanceSection() {
expandedTitle={<T id="expandedTitle" />}
className="col gap-6"
>
<InstanceAlerts selectedCategory={category} />

<InstanceSelector
instances={instances.filter(hasProperty('regionCategory', firstRegion?.category ?? 'koyeb'))}
checkAvailability={(instance) => instanceAvailabilities[instance] ?? [false, 'instanceNotFound']}
selectedInstance={instances.find(hasProperty('identifier', field.value)) ?? null}
previousInstance={previousInstance}
onInstanceSelected={handleInstanceSelected}
onCategoryChanged={setCategory}
className="w-full"
/>
<FeatureFlag feature="new-instance-selector" fallback={<InstanceSelector />}>
<InstanceSelectorNew />
</FeatureFlag>
</ServiceFormSection>
);
}

function SectionTitle() {
const instance = useInstance(useWatchServiceForm('instance'));
const instance = useInstance(useFormContext<ServiceForm>().watch('instance'));
const regions = useRegions(useFormContext<ServiceForm>().watch('regions'));

if (!instance) {
return <T id="noInstanceSelected" />;
Expand All @@ -110,15 +47,40 @@ function SectionTitle() {
);

return (
<>
{instance.displayName}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 font-normal">
<span className="font-medium">{instance.displayName}</span>

<span className="ml-1 inline-flex flex-row items-center gap-4 font-normal">
<span>
<T id="instanceSpec" values={{ spec }} />
<Badge color="green" size={1} className="capitalize">
<T id={`category.${instance.category}`} />
</Badge>
</span>
</>

<Badge color="green" size={1}>
<T id={`category.${instance.category}`} />
</Badge>

<FeatureFlag feature="new-instance-selector">
{regions.length === 1 && (
<span className="inline-flex flex-row items-center gap-2">
<RegionName identifier={regions[0]!.identifier} />
<RegionFlag identifier={regions[0]!.identifier} className="size-em" />
</span>
)}

{regions.length > 1 && (
<span>
{regions.map((region, index) => (
<RegionFlag
key={region.identifier}
identifier={region.identifier}
className="inline-block size-em"
style={{ marginLeft: `-${index / 2}rem` }}
/>
))}

<span className="ml-1">{regions.length} regions</span>
</span>
)}
</FeatureFlag>
</div>
);
}
1 change: 1 addition & 0 deletions src/utils/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const region = createFactory<CatalogRegion>(() => ({
datacenters: [],
hasVolumes: false,
category: 'koyeb',
scope: 'metropolitan',
}));

const instance = createFactory<CatalogInstance>(() => ({
Expand Down

0 comments on commit 04649d2

Please sign in to comment.