Skip to content

Commit

Permalink
check instance quota when submitting the service form
Browse files Browse the repository at this point in the history
  • Loading branch information
nilscox committed Jan 17, 2025
1 parent ce32e40 commit 979d7a7
Show file tree
Hide file tree
Showing 9 changed files with 70 additions and 67 deletions.
46 changes: 46 additions & 0 deletions src/application/instance-quota.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useCallback, useMemo } from 'react';

import { useOrganization, useOrganizationQuotas, useOrganizationSummary } from 'src/api/hooks/session';
import { CatalogInstance } from 'src/api/model';

export function useInstanceQuota(instance: CatalogInstance) {
const getInstanceQuota = useGetInstanceQuota();

return useMemo(() => getInstanceQuota(instance), [getInstanceQuota, instance]);
}

export function useGetInstanceQuota() {
const organization = useOrganization();
const quotas = useOrganizationQuotas();
const summary = useOrganizationSummary();

return useCallback(
(instance: CatalogInstance) => {
const max = () => {
const { maxInstancesByType, instanceTypes } = quotas ?? {};
const quota = maxInstancesByType?.[instance.identifier];

if (quota !== undefined) {
return quota;
}

if (instance.plans && !instance.plans.includes(organization.plan)) {
return 0;
}

if (instanceTypes !== undefined && !instanceTypes.includes(instance.identifier)) {
return 0;
}

return Infinity;
};

const used = () => {
return summary?.instancesUsed[instance.identifier] ?? 0;
};

return { max: max(), used: used() };
},
[organization, quotas, summary],
);
}
38 changes: 3 additions & 35 deletions src/components/instance-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import clsx from 'clsx';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';

import { Badge, Button, DialogFooter, Radio, TabButton, TabButtons } from '@koyeb/design-system';
import { useOrganization, useOrganizationQuotas, useOrganizationSummary } from 'src/api/hooks/session';
import { useOrganization } from 'src/api/hooks/session';
import { CatalogInstance, InstanceCategory } from 'src/api/model';
import { useInstanceQuota } from 'src/application/instance-quota';
import { InstanceAvailability } from 'src/application/instance-region-availability';
import { formatBytes } from 'src/application/memory';
import { useFeatureFlag } from 'src/hooks/feature-flag';
Expand Down Expand Up @@ -230,39 +231,6 @@ function InstanceItem({
);
}

function useInstanceQuota(instance: CatalogInstance) {
const organization = useOrganization();
const quotas = useOrganizationQuotas();
const summary = useOrganizationSummary();

return useMemo(() => {
const max = () => {
const { maxInstancesByType, instanceTypes } = quotas ?? {};
const quota = maxInstancesByType?.[instance.identifier];

if (quota !== undefined) {
return quota;
}

if (instance.plans && !instance.plans.includes(organization.plan)) {
return 0;
}

if (instanceTypes !== undefined && !instanceTypes.includes(instance.identifier)) {
return 0;
}

return Infinity;
};

const used = () => {
return summary?.instancesUsed[instance.identifier] ?? 0;
};

return { max: max(), used: used() };
}, [instance, organization, quotas, summary]);
}

export function RequestQuotaIncreaseDialog({ instance }: { instance: CatalogInstance }) {
const link = 'https://app.reclaim.ai/m/koyeb-intro/short-call';

Expand Down
2 changes: 1 addition & 1 deletion src/intl/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@
"save": "Save",
"noPreviousBuild": "Your service doesn't have any successful build yet"
},
"gpuRestrictedDialog": {
"quotaIncreaseRequestDialog": {
"title": "Quota increase request",
"line1": "It looks like you're ready to deploy on a {instance} instance, but we'll need to increase your organization quota first.",
"line2": "Let's <link>chat</link> to learn more about your needs and get started.",
Expand Down
12 changes: 1 addition & 11 deletions src/modules/service-form/components/quota-alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { keepPreviousData, useQuery } from '@tanstack/react-query';
import { Alert } from '@koyeb/design-system';
import { api } from 'src/api/api';
import { isApiValidationError } from 'src/api/api-errors';
import { useInstance } from 'src/api/hooks/catalog';
import { useOrganizationQuotas } from 'src/api/hooks/session';
import { routes } from 'src/application/routes';
import { useToken } from 'src/application/token';
import { LinkButton } from 'src/components/link';
Expand Down Expand Up @@ -61,15 +59,7 @@ export function QuotaAlert(props: QuotaAlertProps) {
},
});

const instance = useInstance(values.instance);
const quotas = useOrganizationQuotas();

const isRestrictedGpu =
instance?.category === 'gpu' &&
quotas?.maxInstancesByType[instance.identifier] === 0 &&
instance.status === 'restricted';

if (typeof message !== 'string' || isRestrictedGpu) {
if (typeof message !== 'string') {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ import { createTranslate } from 'src/intl/translate';

const T = createTranslate('modules.serviceForm');

export function RestrictedGpuDialog({ instanceIdentifier }: { instanceIdentifier: string | null }) {
export function QuotaIncreaseRequestDialog({ instanceIdentifier }: { instanceIdentifier: string | null }) {
const instance = useInstance(instanceIdentifier);
const link = 'https://app.reclaim.ai/m/koyeb-intro/short-call';

return (
<Dialog id="RestrictedGpu" className="col w-full max-w-xl gap-4">
<DialogHeader title={<T id="gpuRestrictedDialog.title" />} />
<Dialog id="QuotaIncreaseRequest" className="col w-full max-w-xl gap-4">
<DialogHeader title={<T id="quotaIncreaseRequestDialog.title" />} />

<p>
<T id="gpuRestrictedDialog.line1" values={{ instance: instance?.displayName }} />
<T id="quotaIncreaseRequestDialog.line1" values={{ instance: instance?.displayName }} />
</p>

<p>
<T
id="gpuRestrictedDialog.line2"
id="quotaIncreaseRequestDialog.line2"
values={{
link: (children) => (
<ExternalLink openInNewTab href={link} className="underline">
Expand All @@ -32,7 +32,7 @@ export function RestrictedGpuDialog({ instanceIdentifier }: { instanceIdentifier

<DialogFooter>
<ExternalLinkButton openInNewTab href={link}>
<T id="gpuRestrictedDialog.cta" />
<T id="quotaIncreaseRequestDialog.cta" />
</ExternalLinkButton>
</DialogFooter>
</Dialog>
Expand Down
15 changes: 7 additions & 8 deletions src/modules/service-form/helpers/pre-submit-service-form.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useState } from 'react';

import { useOrganization, useOrganizationQuotas } from 'src/api/hooks/session';
import { useOrganization } from 'src/api/hooks/session';
import { CatalogInstance, OrganizationPlan } from 'src/api/model';
import { useGetInstanceQuota } from 'src/application/instance-quota';
import { useTrackEvent } from 'src/application/posthog';
import { Dialog } from 'src/components/dialog';

Expand All @@ -11,16 +12,14 @@ export function usePreSubmitServiceForm() {
const [requiredPlan, setRequiredPlan] = useState<OrganizationPlan>();

const organization = useOrganization();
const quotas = useOrganizationQuotas();
const getInstanceQuota = useGetInstanceQuota();
const trackEvent = useTrackEvent();

return [
requiredPlan,
(instance: CatalogInstance): boolean => {
const isRestrictedGpu =
instance?.category === 'gpu' &&
quotas?.maxInstancesByType[instance.identifier] === 0 &&
instance.status === 'restricted';
const quotas = getInstanceQuota(instance);
const hasQuotas = quotas.used < quotas.max;

if (instance?.category === 'gpu') {
trackEvent('gpu_deployed', { plan: organization.plan, gpu_id: instance.identifier });
Expand All @@ -32,8 +31,8 @@ export function usePreSubmitServiceForm() {
setRequiredPlan(plan);
openDialog(`Upgrade-${plan}`);
return false;
} else if (isRestrictedGpu) {
openDialog('RestrictedGpu');
} else if (!hasQuotas) {
openDialog('QuotaIncreaseRequest');
return false;
}

Expand Down
4 changes: 2 additions & 2 deletions src/modules/service-form/model-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { defined } from 'src/utils/assert';
import { getName, hasProperty } from 'src/utils/object';
import { slugify } from 'src/utils/strings';

import { RestrictedGpuDialog } from './components/restricted-gpu-dialog';
import { QuotaIncreaseRequestDialog } from './components/quota-increase-request-dialog';
import { ServiceFormUpgradeDialog } from './components/service-form-upgrade-dialog';
import { computeEstimatedCost, ServiceCost } from './helpers/estimated-cost';
import { defaultServiceForm } from './helpers/initialize-service-form';
Expand Down Expand Up @@ -145,7 +145,7 @@ function ModelForm_({ model: initialModel, onCostChanged }: ModelFormProps) {
</div>
</form>

<RestrictedGpuDialog instanceIdentifier={form.watch('instance')} />
<QuotaIncreaseRequestDialog instanceIdentifier={form.watch('instance')} />
<ServiceFormUpgradeDialog plan={requiredPlan} submitForm={() => formRef.current?.requestSubmit()} />
</>
);
Expand Down
4 changes: 2 additions & 2 deletions src/modules/service-form/one-click-app-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
ScalingMetadata,
} from '../deployment/metadata/runtime-metadata';

import { RestrictedGpuDialog } from './components/restricted-gpu-dialog';
import { QuotaIncreaseRequestDialog } from './components/quota-increase-request-dialog';
import { ServiceFormUpgradeDialog } from './components/service-form-upgrade-dialog';
import { computeEstimatedCost, ServiceCost } from './helpers/estimated-cost';
import { generateAppName } from './helpers/generate-app-name';
Expand Down Expand Up @@ -148,7 +148,7 @@ function OneClickAppForm_({ onCostChanged }: OneClickAppFormProps) {
</div>
</form>

<RestrictedGpuDialog instanceIdentifier={form.watch('instance')} />
<QuotaIncreaseRequestDialog instanceIdentifier={form.watch('instance')} />
<ServiceFormUpgradeDialog plan={requiredPlan} submitForm={() => formRef.current?.requestSubmit()} />
</>
);
Expand Down
4 changes: 2 additions & 2 deletions src/modules/service-form/service-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { handleSubmit, useFormErrorHandler, useFormValues } from 'src/hooks/form

import { GpuAlert } from './components/gpu-alert';
import { QuotaAlert } from './components/quota-alert';
import { RestrictedGpuDialog } from './components/restricted-gpu-dialog';
import { QuotaIncreaseRequestDialog } from './components/quota-increase-request-dialog';
import { ServiceFormSkeleton } from './components/service-form-skeleton';
import { ServiceFormUpgradeDialog } from './components/service-form-upgrade-dialog';
import { SubmitButton } from './components/submit-button';
Expand Down Expand Up @@ -138,7 +138,7 @@ function ServiceForm_({
</form>
</FormProvider>

<RestrictedGpuDialog instanceIdentifier={form.watch('instance')} />
<QuotaIncreaseRequestDialog instanceIdentifier={form.watch('instance')} />
<ServiceFormUpgradeDialog plan={requiredPlan} submitForm={() => formRef.current?.requestSubmit()} />
</>
);
Expand Down

0 comments on commit 979d7a7

Please sign in to comment.