Skip to content

Commit

Permalink
infer the volumes and snapshot types from the api
Browse files Browse the repository at this point in the history
  • Loading branch information
nilscox committed Jan 30, 2025
1 parent 799d34a commit 568b5af
Show file tree
Hide file tree
Showing 16 changed files with 109 additions and 149 deletions.
4 changes: 1 addition & 3 deletions src/api/from-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import { identity } from 'src/utils/generic';
import { snakeToCamelDeep } from 'src/utils/object';
import { Extend, RequiredDeep, SnakeToCamelCaseDeep } from 'src/utils/types';

export type FromApi<T, NullableFields extends keyof T = never> = Simplify<
SnakeToCamelCaseDeep<Extend<RequiredDeep<T>, { [K in NullableFields]-?: T[K] | null }>>
>;
export type FromApi<T, U = unknown> = Simplify<SnakeToCamelCaseDeep<Extend<RequiredDeep<T>, U>>>;

export function fromApi<T>(obj: T): FromApi<T>;
export function fromApi<T, U>(obj: T, transform: (value: T) => U): FromApi<U>;
Expand Down
4 changes: 2 additions & 2 deletions src/api/hooks/volume.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useQuery } from '@tanstack/react-query';

import { mapVolumesList } from '../mappers/volume';
import { fromApi } from '../from-api';
import { useApiQueryFn } from '../use-api';

export function useVolumesQuery(region?: string) {
return useQuery({
...useApiQueryFn('listVolumes', { query: { limit: '100', region } }),
select: mapVolumesList,
select: ({ volumes }) => fromApi(volumes!),
});
}

Expand Down
40 changes: 0 additions & 40 deletions src/api/mappers/volume.ts

This file was deleted.

39 changes: 6 additions & 33 deletions src/api/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ export type LogicalDatabase = {

// domains

export type Domain = FromApi<Api.Domain, 'verified_at'>;
export type Domain = FromApi<Api.Domain, { verified_at: string | null }>;

// git

Expand Down Expand Up @@ -625,36 +625,9 @@ export type Address = {

// volumes

export type Volume = {
id: string;
status: VolumeStatus;
name: string;
region: string;
size: number;
snapshotId?: string;
serviceId?: string;
createdAt: string;
};

export type VolumeStatus = 'invalid' | 'attached' | 'detached' | 'deleting' | 'deleted';

export type VolumeSnapshot = {
id: string;
volumeId: string;
name: string;
size: number;
region: string;
status: VolumeSnapshotStatus;
type: VolumeSnapshotType;
createdAt: string;
};

export type VolumeSnapshotStatus =
| 'invalid'
| 'creating'
| 'available'
| 'migrating'
| 'deleting'
| 'deleted';
export type Volume = FromApi<
Api.PersistentVolume,
{ snapshot_id?: string; service_id?: string; deleted_at: string | null }
>;

export type VolumeSnapshotType = 'invalid' | 'local' | 'remote';
export type VolumeSnapshot = FromApi<Api.Snapshot>;
49 changes: 27 additions & 22 deletions src/components/status-badges.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import clsx from 'clsx';
import { ComponentProps } from 'react';

import { Badge, BadgeColor, Spinner } from '@koyeb/design-system';
import {
DeploymentStatus,
InstanceStatus,
ServiceStatus,
VolumeSnapshotStatus,
VolumeStatus,
} from 'src/api/model';
import { DeploymentStatus, InstanceStatus, ServiceStatus, Volume, VolumeSnapshot } from 'src/api/model';
import {
IconCircleAlert,
IconCircleCheck,
Expand All @@ -18,17 +13,20 @@ import {
IconMoon,
IconTrash,
} from 'src/components/icons';
import { TranslateEnum } from 'src/intl/translate';

type ResourceStatusProps<Status> = {
ref?: React.Ref<React.ComponentRef<typeof Badge>>;
status: Status;
// todo: make enum required
enum?: React.ComponentProps<typeof TranslateEnum>['enum'];
className?: string;
};

function createResourceStatus<Status extends string>(
map: Record<Status, [React.ComponentType<{ className?: string }>, BadgeColor]>,
) {
return function ResourceStatus({ ref, status, className }: ResourceStatusProps<Status>) {
return function ResourceStatus({ ref, status, enum: enumName, className }: ResourceStatusProps<Status>) {
const [Icon, color] = map[status] ?? unknownStatusBadge;

return (
Expand All @@ -39,7 +37,14 @@ function createResourceStatus<Status extends string>(
className={clsx('inline-flex flex-row items-center gap-1', className)}
>
<Icon className="size-4" />
<span className="capitalize">{status}</span>

{enumName ? (
<span>
<TranslateEnum enum={enumName} value={status as ComponentProps<typeof TranslateEnum>['value']} />
</span>
) : (
<span className="capitalize">{status}</span>
)}
</Badge>
);
};
Expand Down Expand Up @@ -89,19 +94,19 @@ export const InstanceStatusBadge = createResourceStatus<InstanceStatus>({
sleeping: [IconMoon, 'gray'],
});

export const VolumeStatusBadge = createResourceStatus<VolumeStatus>({
invalid: [IconCircleX, 'red'],
attached: [IconCircleCheck, 'green'],
detached: [IconCircleCheck, 'blue'],
deleting: [Spinner, 'orange'],
deleted: [IconTrash, 'red'],
export const VolumeStatusBadge = createResourceStatus<Volume['status']>({
PERSISTENT_VOLUME_STATUS_INVALID: [IconCircleX, 'red'],
PERSISTENT_VOLUME_STATUS_ATTACHED: [IconCircleCheck, 'green'],
PERSISTENT_VOLUME_STATUS_DETACHED: [IconCircleCheck, 'blue'],
PERSISTENT_VOLUME_STATUS_DELETING: [Spinner, 'orange'],
PERSISTENT_VOLUME_STATUS_DELETED: [IconTrash, 'red'],
});

export const VolumeSnapshotStatusBadge = createResourceStatus<VolumeSnapshotStatus>({
invalid: [IconCircleX, 'red'],
creating: [Spinner, 'gray'],
available: [IconCircleCheck, 'green'],
migrating: [Spinner, 'blue'],
deleting: [Spinner, 'orange'],
deleted: [IconTrash, 'red'],
export const VolumeSnapshotStatusBadge = createResourceStatus<VolumeSnapshot['status']>({
SNAPSHOT_STATUS_INVALID: [IconCircleX, 'red'],
SNAPSHOT_STATUS_CREATING: [Spinner, 'gray'],
SNAPSHOT_STATUS_AVAILABLE: [IconCircleCheck, 'green'],
SNAPSHOT_STATUS_MIGRATING: [Spinner, 'blue'],
SNAPSHOT_STATUS_DELETING: [Spinner, 'orange'],
SNAPSHOT_STATUS_DELETED: [IconTrash, 'red'],
});
15 changes: 15 additions & 0 deletions src/intl/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@
"enterprise": "Enterprise",
"internal": "Internal",
"no_plan": "No plan"
},
"volumeStatus": {
"PERSISTENT_VOLUME_STATUS_INVALID": "Invalid",
"PERSISTENT_VOLUME_STATUS_ATTACHED": "Attached",
"PERSISTENT_VOLUME_STATUS_DETACHED": "Detached",
"PERSISTENT_VOLUME_STATUS_DELETING": "Deleting",
"PERSISTENT_VOLUME_STATUS_DELETED": "DeleteD"
},
"snapshotStatus": {
"SNAPSHOT_STATUS_INVALID": "Invalid",
"SNAPSHOT_STATUS_CREATING": "Creating",
"SNAPSHOT_STATUS_AVAILABLE": "Available",
"SNAPSHOT_STATUS_MIGRATING": "Migrating",
"SNAPSHOT_STATUS_DELETING": "Deleting",
"SNAPSHOT_STATUS_DELETED": "Deleted"
}
},
"components": {
Expand Down
18 changes: 11 additions & 7 deletions src/intl/translate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,18 @@ export function Translate({ id, values }: { id: TranslationKeys; values?: Values
return <>{translate(id, values)}</>;
}

type Enums =
Extract<TranslationKeys, `enums.${string}.${string}`> extends `enums.${infer E}.${infer V}`
? [E, V]
: never;
type EnumKeys<E extends string = string> = Extract<TranslationKeys, `enums.${E}.${string}`>;
type Enums = EnumKeys extends `enums.${infer K}.${string}` ? K : never;

type Enum = Enums extends [infer Enum, string] ? Enum : never;
type EnumValue<E extends Enum> = Enums extends [E, infer V] ? V : never;
type EnumValues = {
[Key in Enums]: EnumKeys<Key> extends `enums.${Key}.${infer V}` ? V : never;
};

export function TranslateEnum<E extends Enum>({ enum: enumName, value }: { enum: E; value: EnumValue<E> }) {
type TranslateEnumProps<E extends Enums> = {
enum: E;
value: EnumValues[E];
};

export function TranslateEnum<E extends Enums>({ enum: enumName, value }: TranslateEnumProps<E>) {
return <Translate id={`enums.${enumName}.${value}` as TranslationKeys} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useForm } from 'react-hook-form';
import { z } from 'zod';

import { Button, InputEnd } from '@koyeb/design-system';
import { mapVolume } from 'src/api/mappers/volume';
import { fromApi } from 'src/api/from-api';
import { Volume } from 'src/api/model';
import { useApiMutationFn, useInvalidateApiQuery } from 'src/api/use-api';
import { withStopPropagation } from 'src/application/dom-events';
Expand Down Expand Up @@ -56,7 +56,7 @@ export function CreateVolumeDialog({ onCreated }: CreateVolumeDialogProps) {
})),
async onSuccess({ volume }, { mountPath }) {
await invalidate('listVolumes');
onCreated(mapVolume(volume!), mountPath);
onCreated(fromApi(volume!), mountPath);
},
onError: useFormErrorHandler(form, (error) => ({
name: error.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useFieldArray, useFormContext } from 'react-hook-form';

import { Alert, Button } from '@koyeb/design-system';
import { useInstance, useRegions } from 'src/api/hooks/catalog';
import { parseBytes } from 'src/application/memory';
import { Dialog } from 'src/components/dialog';
import { DocumentationLink } from 'src/components/documentation-link';
import { IconPlus } from 'src/components/icons';
Expand Down Expand Up @@ -90,7 +91,14 @@ function SectionContent() {

<CreateVolumeDialog
onCreated={(volume, mountPath) => {
append({ volumeId: volume.id, name: volume.name, size: volume.size, mountPath, mounted: false });
append({
volumeId: volume.id,
name: volume.name,
size: parseBytes(`${volume.maxSize}GB`),
mountPath,
mounted: false,
});

closeDialog();
}}
/>
Expand Down
10 changes: 9 additions & 1 deletion src/modules/service-form/sections/08-volumes/volumes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useFieldArray, UseFieldArrayReturn, useFormContext } from 'react-hook-f

import { Alert, Button, InfoTooltip } from '@koyeb/design-system';
import { useInstance, useRegions } from 'src/api/hooks/catalog';
import { parseBytes } from 'src/application/memory';
import { Dialog } from 'src/components/dialog';
import { DocumentationLink } from 'src/components/documentation-link';
import { IconPlus } from 'src/components/icons';
Expand Down Expand Up @@ -90,7 +91,14 @@ function VolumesList({ fields, append, remove }: VolumesListProps) {

<CreateVolumeDialog
onCreated={(volume, mountPath) => {
append({ volumeId: volume.id, name: volume.name, size: volume.size, mountPath, mounted: false });
append({
volumeId: volume.id,
name: volume.name,
size: parseBytes(`${volume.maxSize}GB`),
mountPath,
mounted: false,
});

closeDialog();
}}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/volumes/create-volume-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function CreateVolumeDialog({ snapshot }: { snapshot?: VolumeSnapshot })
const closeDialog = Dialog.useClose();

return (
<Dialog id="CreateVolume" className="col w-full max-w-xl gap-4">
<Dialog id={`CreateVolume-${snapshot?.id}`} className="col w-full max-w-xl gap-4">
<DialogHeader title={<T id="title" />} />

<p className="text-dim">{<T id="description" />}</p>
Expand Down
18 changes: 6 additions & 12 deletions src/pages/volumes/volume-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { z } from 'zod';

import { InputEnd } from '@koyeb/design-system';
import { api } from 'src/api/api';
import { fromApi } from 'src/api/from-api';
import { useRegion, useRegions } from 'src/api/hooks/catalog';
import { mapVolume } from 'src/api/mappers/volume';
import { Volume, VolumeSnapshot } from 'src/api/model';
import { useInvalidateApiQuery } from 'src/api/use-api';
import { notify } from 'src/application/notify';
Expand All @@ -24,12 +24,6 @@ const schema = z.object({
size: z.number(),
});

function toGigaBytes(bytes: number | undefined) {
if (bytes !== undefined) {
return bytes / Math.pow(1000, 3);
}
}

type VolumeFormProps = {
snapshot?: VolumeSnapshot;
volume?: Volume;
Expand All @@ -48,7 +42,7 @@ export function VolumeForm({ snapshot, volume, onSubmitted, renderFooter }: Volu
defaultValues: {
name: volume?.name ?? '',
region: snapshot?.region ?? volume?.region ?? '',
size: toGigaBytes(snapshot?.size ?? volume?.size) ?? NaN,
size: snapshot?.size ?? volume?.maxSize ?? NaN,
},
resolver: useZodResolver(schema, {
name: t('nameLabel'),
Expand All @@ -66,7 +60,7 @@ export function VolumeForm({ snapshot, volume, onSubmitted, renderFooter }: Volu
path: { id: volume.id },
body: { name },
})
.then(({ volume }) => mapVolume(volume!));
.then(({ volume }) => fromApi(volume!));
} else {
return api
.createVolume({
Expand All @@ -79,7 +73,7 @@ export function VolumeForm({ snapshot, volume, onSubmitted, renderFooter }: Volu
region,
},
})
.then(({ volume }) => mapVolume(volume!));
.then(({ volume }) => fromApi(volume!));
}
},
async onSuccess(createVolume) {
Expand Down Expand Up @@ -113,7 +107,7 @@ export function VolumeForm({ snapshot, volume, onSubmitted, renderFooter }: Volu
<ControlledSelect
control={form.control}
name="region"
disabled={snapshot?.type === 'local' || volume !== undefined}
disabled={snapshot?.type === 'SNAPSHOT_TYPE_LOCAL' || volume !== undefined}
label={<T id="regionLabel" />}
placeholder={t('regionPlaceholder')}
items={regions}
Expand All @@ -122,7 +116,7 @@ export function VolumeForm({ snapshot, volume, onSubmitted, renderFooter }: Volu
itemToValue={(region) => region.identifier}
renderItem={(region) => region.displayName}
helperText={
snapshot?.type === 'local' && (
snapshot?.type === 'SNAPSHOT_TYPE_LOCAL' && (
<T id="regionBoundedToLocalSnapshot" values={{ region: snapshotRegion?.displayName }} />
)
}
Expand Down
Loading

0 comments on commit 568b5af

Please sign in to comment.