Skip to content
Closed
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: 10 additions & 0 deletions .server-changes/admin-concurrency-quota.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
area: webapp
type: feature
---

Admin Back office: editor for an org's concurrency quota cap (the per-org
override on how much extra concurrency the org can purchase). Sits as a new
section on the existing per-org back-office page alongside API/Batch rate
limits and Maximum projects. Calls cloud's billing service to update
billing.Limits.extraConcurrencyQuota.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { z } from "zod";
import { logger } from "~/services/logger.server";
import { setExtraConcurrencyQuota } from "~/services/platform.v3.server";
import { CONCURRENCY_QUOTA_INTENT } from "./ConcurrencyQuotaSection";

const SetConcurrencyQuotaSchema = z.object({
intent: z.literal(CONCURRENCY_QUOTA_INTENT),
// Capped at PostgreSQL INTEGER max for safety; cloud will reject anything
// unreasonably high on its own (likely with quota_too_high).
extraConcurrencyQuota: z.coerce.number().int().min(0).max(2_147_483_647),
});

export type ConcurrencyQuotaActionResult =
| { ok: true }
| {
ok: false;
errors: Record<string, string[] | undefined>;
formError?: string;
};

export async function handleConcurrencyQuotaAction(
formData: FormData,
orgId: string,
adminUserId: string
): Promise<ConcurrencyQuotaActionResult> {
const submission = SetConcurrencyQuotaSchema.safeParse(
Object.fromEntries(formData)
);
if (!submission.success) {
return { ok: false, errors: submission.error.flatten().fieldErrors };
}

const result = await setExtraConcurrencyQuota(orgId, {
extraConcurrencyQuota: submission.data.extraConcurrencyQuota,
});

if (!result) {
return {
ok: false,
errors: {},
formError:
"Billing client unavailable — check BILLING_API_URL/BILLING_API_KEY config.",
};
}

if (!result.success) {
// The platform client's generic error path strips `code` to `error` only
// until the BillingClient.fetch passthrough fix lands; cast keeps the
// route forward-compatible so precise UI copy renders automatically once
// it does.
const err = result as {
success: false;
error: string;
code?: string;
};
return {
ok: false,
errors: {},
formError: mapCodeToMessage(err.code, err.error),
};
}

logger.info("admin.backOffice.concurrencyQuota", {
adminUserId,
orgId,
next: submission.data.extraConcurrencyQuota,
});

return { ok: true };
}

function mapCodeToMessage(
code: string | undefined,
fallback: string
): string {
switch (code) {
case "invalid_body":
return "Quota must be a non-negative integer.";
case "quota_too_high":
// Cloud's `error` string embeds the actual ceiling, prefer it verbatim.
return fallback || "Cap is too high.";
case "org_not_found":
return "Organization not found.";
case "limits_not_found":
return "This org has no billing limits row yet.";
default:
return fallback || "Failed to update concurrency quota.";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { Form } from "@remix-run/react";
import { useEffect, useState } from "react";
import { Button } from "~/components/primitives/Buttons";
import { FormError } from "~/components/primitives/FormError";
import { Header2 } from "~/components/primitives/Headers";
import { Input } from "~/components/primitives/Input";
import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
import * as Property from "~/components/primitives/PropertyTable";

export const CONCURRENCY_QUOTA_INTENT = "set-concurrency-quota";
export const CONCURRENCY_QUOTA_SAVED_VALUE = "concurrency-quota";

type FieldErrors = Record<string, string[] | undefined> | null;

type Props = {
currentQuota: number;
purchased: number;
errors: FieldErrors;
formError: string | null;
savedJustNow: boolean;
isSubmitting: boolean;
};

export function ConcurrencyQuotaSection({
currentQuota,
purchased,
errors,
formError,
savedJustNow,
isSubmitting,
}: Props) {
const hasFieldErrors = !!errors && Object.keys(errors).length > 0;
const fieldError = (field: string) =>
errors && field in errors ? errors[field]?.[0] : undefined;

const [isEditing, setIsEditing] = useState(hasFieldErrors || !!formError);
const [value, setValue] = useState(String(currentQuota));

useEffect(() => {
if (hasFieldErrors || formError) setIsEditing(true);
}, [hasFieldErrors, formError]);

useEffect(() => {
if (savedJustNow && !hasFieldErrors && !formError) setIsEditing(false);
}, [savedJustNow, hasFieldErrors, formError]);

const cancelEdit = () => {
setValue(String(currentQuota));
setIsEditing(false);
};

const trimmedValue = value.trim();
const parsed = Number(trimmedValue);
const isValidPreview =
trimmedValue.length > 0 &&
Number.isInteger(parsed) &&
parsed >= 0;
const delta = isValidPreview ? parsed - currentQuota : 0;
const deltaLabel =
delta > 0
? `+${delta.toLocaleString()}`
: delta < 0
? delta.toLocaleString()
: "no change";
const headroomAfter = isValidPreview ? parsed - purchased : 0;

return (
<section className="flex flex-col gap-3 rounded-md border border-charcoal-700 bg-charcoal-800 p-4">
<div className="flex items-center justify-between">
<Header2>Concurrency quota</Header2>
{!isEditing && (
<Button
variant="tertiary/small"
onClick={() => setIsEditing(true)}
disabled={isSubmitting}
>
Edit
</Button>
)}
</div>

<Paragraph variant="small">
Cap on how much extra concurrency this org can purchase. Increases
unlock self-serve purchase up to the new cap; the org still has to
complete the purchase from the billing flow.
</Paragraph>

{savedJustNow && (
<div className="rounded-md border border-green-600/40 bg-green-600/10 px-3 py-2">
<Paragraph variant="small" className="text-green-500">
Saved.
</Paragraph>
</div>
)}

{formError && (
<div className="rounded-md border border-red-600/40 bg-red-600/10 px-3 py-2">
<Paragraph variant="small" className="text-red-500">
{formError}
</Paragraph>
</div>
)}

{!isEditing ? (
<Property.Table>
<Property.Item>
<Property.Label>Max extra concurrency this org can purchase on top of their plan</Property.Label>
<Property.Value>{currentQuota.toLocaleString()}</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Already purchased</Property.Label>
<Property.Value>{purchased.toLocaleString()}</Property.Value>
</Property.Item>
</Property.Table>
) : (
<Form method="post" className="flex flex-col gap-3 pt-2">
<input type="hidden" name="intent" value={CONCURRENCY_QUOTA_INTENT} />

<div className="flex flex-col gap-1">
<Label>Max extra concurrency this org can purchase on top of their plan</Label>
<Input
name="extraConcurrencyQuota"
type="number"
min={0}
value={value}
onChange={(e) => setValue(e.target.value)}
required
/>
<FormError>{fieldError("extraConcurrencyQuota")}</FormError>
</div>

{isValidPreview && (
<div className="rounded-md border border-charcoal-700 bg-charcoal-900 px-3 py-2">
<Paragraph variant="small">
Cap: {currentQuota.toLocaleString()} →{" "}
{parsed.toLocaleString()} ({deltaLabel})
</Paragraph>
<Paragraph variant="small" className="text-text-dimmed">
Already purchased: {purchased.toLocaleString()}
</Paragraph>
{headroomAfter >= 0 ? (
<Paragraph variant="small" className="text-text-dimmed">
After save: {headroomAfter.toLocaleString()} more buyable.
</Paragraph>
) : (
<Paragraph variant="small" className="text-amber-500">
Below already-purchased — org would be{" "}
{(-headroomAfter).toLocaleString()} over the new cap. They'd
keep what they have but couldn't buy more until you raise it.
</Paragraph>
)}
</div>
)}

<div className="flex items-center gap-2">
<Button
type="submit"
variant="primary/medium"
disabled={isSubmitting || !value.trim()}
>
Save
</Button>
<Button
type="button"
variant="tertiary/medium"
onClick={cancelEdit}
disabled={isSubmitting}
>
Cancel
</Button>
</div>
</Form>
)}
</section>
);
}
55 changes: 52 additions & 3 deletions apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import {
handleBatchRateLimitAction,
resolveEffectiveBatchRateLimit,
} from "~/components/admin/backOffice/BatchRateLimitSection.server";
import {
CONCURRENCY_QUOTA_INTENT,
CONCURRENCY_QUOTA_SAVED_VALUE,
ConcurrencyQuotaSection,
} from "~/components/admin/backOffice/ConcurrencyQuotaSection";
import { handleConcurrencyQuotaAction } from "~/components/admin/backOffice/ConcurrencyQuotaSection.server";
import {
MAX_PROJECTS_INTENT,
MAX_PROJECTS_SAVED_VALUE,
Expand All @@ -36,6 +42,7 @@ import { CopyableText } from "~/components/primitives/CopyableText";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { prisma } from "~/db.server";
import { getCurrentPlan } from "~/services/platform.v3.server";
import { requireUser } from "~/services/session.server";

const SAVED_QUERY_KEY = "saved";
Expand Down Expand Up @@ -73,7 +80,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
org.batchRateLimitConfig
);

return typedjson({ org, apiEffective, batchEffective });
const currentPlan = await getCurrentPlan(org.id);
const concurrencyAddOn = currentPlan?.v3Subscription?.addOns?.concurrentRuns;
const concurrencyQuota = {
currentQuota: concurrencyAddOn?.quota ?? 0,
purchased: concurrencyAddOn?.purchased ?? 0,
};

return typedjson({ org, apiEffective, batchEffective, concurrencyQuota });
}

export async function action({ request, params }: ActionFunctionArgs) {
Expand Down Expand Up @@ -129,14 +143,31 @@ export async function action({ request, params }: ActionFunctionArgs) {
);
}

if (intent === CONCURRENCY_QUOTA_INTENT) {
const result = await handleConcurrencyQuotaAction(formData, orgId, user.id);
if (!result.ok) {
return typedjson(
{
section: CONCURRENCY_QUOTA_SAVED_VALUE,
errors: result.errors,
formError: result.formError ?? null,
},
{ status: 400 }
);
}
return redirect(
`/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${CONCURRENCY_QUOTA_SAVED_VALUE}`
);
}

return typedjson(
{ section: null, errors: { intent: ["Unknown intent."] } },
{ status: 400 }
);
}

export default function BackOfficeOrgPage() {
const { org, apiEffective, batchEffective } =
const { org, apiEffective, batchEffective, concurrencyQuota } =
useTypedLoaderData<typeof loader>();
const actionData = useTypedActionData<typeof action>();
const navigation = useNavigation();
Expand All @@ -147,13 +178,20 @@ export default function BackOfficeOrgPage() {
navigation.state !== "idle" && submittingIntent === BATCH_RATE_LIMIT_INTENT;
const isSubmittingMaxProjects =
navigation.state !== "idle" && submittingIntent === MAX_PROJECTS_INTENT;
const isSubmittingConcurrencyQuota =
navigation.state !== "idle" &&
submittingIntent === CONCURRENCY_QUOTA_INTENT;

const errorSection =
actionData && "section" in actionData ? actionData.section : null;
const errors =
actionData && "errors" in actionData
? (actionData.errors as Record<string, string[] | undefined>)
: null;
const formError =
actionData && "formError" in actionData
? ((actionData as { formError?: string | null }).formError ?? null)
: null;

const [searchParams, setSearchParams] = useSearchParams();
const savedSectionRaw = searchParams.get(SAVED_QUERY_KEY);
Expand All @@ -179,7 +217,7 @@ export default function BackOfficeOrgPage() {
}, [savedSection, setSearchParams]);

return (
<div className="flex flex-col gap-6 py-4">
<div className="flex shrink-0 flex-col gap-6 pb-12 pt-4">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<Header1>{org.title}</Header1>
Expand Down Expand Up @@ -212,6 +250,17 @@ export default function BackOfficeOrgPage() {
savedJustNow={savedSection === MAX_PROJECTS_SAVED_VALUE}
isSubmitting={isSubmittingMaxProjects}
/>

<ConcurrencyQuotaSection
currentQuota={concurrencyQuota.currentQuota}
purchased={concurrencyQuota.purchased}
errors={errorSection === CONCURRENCY_QUOTA_SAVED_VALUE ? errors : null}
formError={
errorSection === CONCURRENCY_QUOTA_SAVED_VALUE ? formError : null
}
savedJustNow={savedSection === CONCURRENCY_QUOTA_SAVED_VALUE}
isSubmitting={isSubmittingConcurrencyQuota}
/>
</div>
);
}
Loading
Loading