Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New instance selector component #216

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/api/mappers/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@ import {
CatalogInstanceStatus,
CatalogRegion,
InstanceCategory,
RegionScope,
} from '../model';

export function mapCatalogRegionsList({ regions }: ApiEndpointResult<'listCatalogRegions'>): CatalogRegion[] {
return regions!.map((region) => ({
return regions!.map(mapCatalogRegion);
}

export function mapCatalogRegion(region: Api.CatalogRegion): CatalogRegion {
return {
identifier: region.id!,
displayName: region.name!,
status: lowerCase(region.status!) as CatalogRegion['status'],
datacenters: region.datacenters!,
instances: region.instances,
hasVolumes: region.volumes_enabled!,
category: region.id!.startsWith('aws-') ? 'aws' : 'koyeb',
}));
scope: region.scope as RegionScope,
};
}

export function mapCatalogDatacentersList({
Expand Down
1 change: 1 addition & 0 deletions src/api/mock/api-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const createApiRegion = createFactory<Api.CatalogRegion>(() => ({
id: createId(),
name: '',
status: 'AVAILABLE',
datacenters: [],
}));

export const createApiDatacenter = createFactory<Api.CatalogDatacenters>(() => ({
Expand Down
172 changes: 92 additions & 80 deletions src/api/mock/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,110 +1,122 @@
import { createApiCatalogInstance, createApiDatacenter, createApiRegion } from './api-factories';

const ecoInstanceIds = [
'eco-2xlarge',
'eco-large',
'eco-medium',
'eco-micro',
'eco-nano',
'eco-small',
'eco-xlarge',
];

const standardInstanceIds = [
'2xlarge',
'3xlarge',
'4xlarge',
'5xlarge',
'large',
'medium',
'micro',
'nano',
'small',
'xlarge',
];

const gpuInstanceIds = [
'2-gpu-nvidia-a100',
'4-gpu-nvidia-a100',
'8-gpu-nvidia-a100',
'gpu-nvidia-a100',
'gpu-nvidia-rtx-4000-sff-ada',
];

export const catalogRegionFixtures = [
createApiRegion({
id: 'bxl',
name: 'Brussels',
coordinates: ['4.3517', '50.8503'],
status: 'COMING_SOON',
instances: [],
datacenters: [],
}),
createApiRegion({
id: 'fra',
name: 'Frankfurt',
coordinates: ['8.6821', '50.1109'],
status: 'AVAILABLE',
instances: [
'2-gpu-nvidia-l4',
'2xlarge',
'3xlarge',
'4xlarge',
'5xlarge',
'eco-2xlarge',
'eco-large',
'eco-medium',
'eco-micro',
'eco-nano',
'eco-small',
'eco-xlarge',
'free',
'gpu-nvidia-l4',
'gpu-nvidia-rtx-4000-sff-ada',
'large',
'medium',
'micro',
'nano',
'small',
'xlarge',
],
datacenters: ['fra1'],
}),
createApiRegion({
id: 'maa',
name: 'Chennai',
coordinates: ['80.237617', '13.067439'],
status: 'COMING_SOON',
instances: [],
datacenters: [],
datacenters: ['fra1', 'fra2', 'fsn1'],
instances: ['free', ...ecoInstanceIds, ...standardInstanceIds, ...gpuInstanceIds],
scope: 'metropolitan',
volumes_enabled: true,
}),
createApiRegion({
id: 'par',
name: 'Paris',
coordinates: ['2.3522', '48.8566'],
status: 'AVAILABLE',
instances: [
'2xlarge',
'3xlarge',
'4xlarge',
'5xlarge',
'large',
'medium',
'micro',
'nano',
'small',
'xlarge',
],
datacenters: ['par1'],
}),
createApiRegion({
id: 'sao',
name: 'Sao Paulo',
coordinates: ['-46.625290', '-23.533773'],
status: 'COMING_SOON',
instances: [],
datacenters: [],
instances: [...standardInstanceIds, ...gpuInstanceIds],
scope: 'metropolitan',
volumes_enabled: true,
}),
createApiRegion({
id: 'sfo',
name: 'San Francisco',
coordinates: ['-122.419418', '37.774929'],
status: 'COMING_SOON',
instances: [],
datacenters: [],
datacenters: ['sfo1', 'sfo2'],
instances: standardInstanceIds,
scope: 'metropolitan',
volumes_enabled: false,
}),
createApiRegion({
id: 'sin',
name: 'Singapore',
coordinates: ['103.819839', '1.352083'],
status: 'COMING_SOON',
instances: [],
datacenters: [],
datacenters: ['sin1'],
instances: [...ecoInstanceIds, ...standardInstanceIds],
scope: 'metropolitan',
volumes_enabled: false,
}),
createApiRegion({
id: 'tyo',
name: 'Tokyo',
coordinates: ['139.817413', '35.672855'],
status: 'COMING_SOON',
instances: [],
datacenters: [],
datacenters: ['tyo1'],
instances: standardInstanceIds,
scope: 'metropolitan',
volumes_enabled: false,
}),
createApiRegion({
id: 'was',
name: 'Washington, D.C',
coordinates: ['-77.007507', '38.900497'],
status: 'COMING_SOON',
instances: [],
datacenters: [],
datacenters: ['rdu1', 'was1'],
instances: ['free', ...ecoInstanceIds, ...standardInstanceIds, ...gpuInstanceIds],
scope: 'metropolitan',
volumes_enabled: true,
}),
createApiRegion({
id: 'na',
name: 'North America',
datacenters: ['rdu1', 'was1'],
scope: 'continental',
volumes_enabled: true,
}),
createApiRegion({
id: 'sa',
name: 'South America',
scope: 'continental',
volumes_enabled: true,
}),
createApiRegion({
id: 'nea',
name: 'North-East Asia',
scope: 'continental',
volumes_enabled: true,
}),
createApiRegion({
id: 'sea',
name: 'South-East Asia',
scope: 'continental',
volumes_enabled: true,
}),
createApiRegion({
id: 'af',
name: 'Africa',
scope: 'continental',
volumes_enabled: true,
}),
createApiRegion({
id: 'eu',
name: 'Europe',
datacenters: ['fra1'],
scope: 'continental',
volumes_enabled: true,
}),
];

Expand Down
2 changes: 2 additions & 0 deletions src/api/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,11 @@ export type CatalogRegion = {
instances?: string[];
hasVolumes: boolean;
category: RegionCategory;
scope: RegionScope;
};

export type RegionCategory = 'koyeb' | 'aws';
export type RegionScope = 'continental' | 'metropolitan';

export type OneClickApp = {
name: string;
Expand Down
27 changes: 19 additions & 8 deletions src/application/instance-quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,27 @@ export function useInstanceQuota(instance: CatalogInstance) {
}

export function useHasInstanceQuota(instance: CatalogInstance, previousInstance?: CatalogInstance) {
const quota = useInstanceQuota(instance);
const hasQuota = useGetHasInstanceQuota(previousInstance);

return useMemo(() => {
// allow keeping the same instance
if (previousInstance?.identifier === instance.identifier) {
return true;
}
return useMemo(() => hasQuota(instance), [instance, hasQuota]);
}

export function useGetHasInstanceQuota(previousInstance?: CatalogInstance) {
const getQuota = useGetInstanceQuota();

return useCallback(
(instance: CatalogInstance) => {
const quota = getQuota(instance);

// allow keeping the same instance
if (previousInstance?.identifier === instance.identifier) {
return true;
}

return quota.used < quota.max;
}, [instance, previousInstance, quota]);
return quota.used < quota.max;
},
[getQuota, previousInstance],
);
}

export function useGetInstanceQuota() {
Expand Down
63 changes: 50 additions & 13 deletions src/application/instance-region-availability.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';

import { useInstances, useRegion, useRegions } from 'src/api/hooks/catalog';
import { useOrganizationSummary } from 'src/api/hooks/session';
Expand All @@ -11,27 +11,48 @@ export type RegionAvailability = [available: true] | [available: false, reason:

export type RegionUnavailableReason = 'unavailable' | 'regionNotFound' | 'unavailableForInstance';

export function useRegionAvailabilities() {
export function useRegionAvailabilities(options?: CheckRegionAvailabilityOptions) {
const regions = useRegions();
const optionsMemo = useDeepCompareMemo(options);

return useMemo(() => {
return toObject(
regions,
(region) => region.identifier,
(region) => checkRegionAvailability(region),
(region) => checkRegionAvailability(region, optionsMemo),
);
}, [regions]);
}, [regions, optionsMemo]);
}

export function useIsRegionAvailable(options?: CheckRegionAvailabilityOptions) {
const availabilities = useRegionAvailabilities(options);

return useCallback(
({ identifier }: CatalogRegion) => Boolean(availabilities[identifier]?.[0]),
[availabilities],
);
}

export function useRegionAvailability(regionIdentifier: string) {
return useRegionAvailabilities()[regionIdentifier] ?? [false, 'regionNotFound'];
}

function checkRegionAvailability(region: CatalogRegion): RegionAvailability {
export type CheckRegionAvailabilityOptions = {
instance?: CatalogInstance;
};

function checkRegionAvailability(
region: CatalogRegion,
options: CheckRegionAvailabilityOptions = {},
): RegionAvailability {
if (region.status !== 'available') {
return [false, 'unavailable'];
}

if (options.instance && region.instances && !region.instances.includes(options.instance.identifier)) {
return [false, 'unavailableForInstance'];
}

return [true];
}

Expand Down Expand Up @@ -61,19 +82,35 @@ export type InstanceUnavailableReason =

export function useInstanceAvailabilities(options: CheckInstanceAvailabilityOptions = {}) {
const instances = useInstances();
const check = useCheckInstanceAvailability(options);

return useMemo(() => {
return toObject(instances, (instance) => instance.identifier, check);
}, [instances, check]);
}

export function useIsInstanceAvailable(options: CheckInstanceAvailabilityOptions = {}) {
const availabilities = useInstanceAvailabilities(options);

return useCallback(
({ identifier }: CatalogInstance) => Boolean(availabilities[identifier]?.[0]),
[availabilities],
);
}

export function useCheckInstanceAvailability(options: CheckInstanceAvailabilityOptions = {}) {
const organizationSummary = useOrganizationSummary();
const optionsMemo = useDeepCompareMemo(options);

return useMemo(() => {
return toObject(
instances,
(instance) => instance.identifier,
(instance) => checkInstanceAvailability(instance, organizationSummary, optionsMemo),
);
}, [instances, organizationSummary, optionsMemo]);
return useCallback(
(instance: CatalogInstance) => {
return checkInstanceAvailability(instance, organizationSummary, optionsMemo);
},
[organizationSummary, optionsMemo],
);
}

type CheckInstanceAvailabilityOptions = {
export type CheckInstanceAvailabilityOptions = {
serviceType?: ServiceType;
hasVolumes?: boolean;
previousInstance?: CatalogInstance;
Expand Down
2 changes: 1 addition & 1 deletion src/components/activity/activity-resources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ function ServiceResource({
function RegionResource({ identifier }: { identifier: string }) {
return (
<ActivityResource className="min-w-max">
<RegionFlag identifier={identifier} className="size-4 rounded-full shadow-badge" />
<RegionFlag identifier={identifier} className="size-4" />
<span className="uppercase">{identifier}</span>
</ActivityResource>
);
Expand Down
Loading
Loading