Skip to content

Commit 9eef605

Browse files
committed
feat(webapp): admin editor for org concurrency quota
1 parent 8c01a06 commit 9eef605

6 files changed

Lines changed: 377 additions & 4 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Admin Back office: editor for an org's concurrency quota cap (the per-org
7+
override on how much extra concurrency the org can purchase). Sits as a new
8+
section on the existing per-org back-office page alongside API/Batch rate
9+
limits and Maximum projects. Calls cloud's billing service to update
10+
billing.Limits.extraConcurrencyQuota.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { z } from "zod";
2+
import { logger } from "~/services/logger.server";
3+
import { setExtraConcurrencyQuota } from "~/services/platform.v3.server";
4+
import { CONCURRENCY_QUOTA_INTENT } from "./ConcurrencyQuotaSection";
5+
6+
const RawSchema = z.object({
7+
intent: z.literal(CONCURRENCY_QUOTA_INTENT),
8+
// Checkbox arrives as "on" / "true" when checked, absent when not.
9+
usePlanDefault: z.string().optional(),
10+
// Empty string when "Use plan default" is checked (the input is disabled).
11+
extraConcurrencyQuota: z.string().optional(),
12+
});
13+
14+
const SetConcurrencyQuotaSchema = RawSchema.transform((raw, ctx) => {
15+
const usePlanDefault = !!raw.usePlanDefault;
16+
if (usePlanDefault) {
17+
return { extraConcurrencyQuota: null as number | null };
18+
}
19+
const trimmed = (raw.extraConcurrencyQuota ?? "").trim();
20+
if (trimmed.length === 0) {
21+
ctx.addIssue({
22+
code: z.ZodIssueCode.custom,
23+
message: "Enter a non-negative integer or check 'Use plan default'.",
24+
path: ["extraConcurrencyQuota"],
25+
});
26+
return z.NEVER;
27+
}
28+
const parsed = Number(trimmed);
29+
if (!Number.isInteger(parsed) || parsed < 0) {
30+
ctx.addIssue({
31+
code: z.ZodIssueCode.custom,
32+
message: "Quota must be a non-negative integer.",
33+
path: ["extraConcurrencyQuota"],
34+
});
35+
return z.NEVER;
36+
}
37+
return { extraConcurrencyQuota: parsed as number | null };
38+
});
39+
40+
export type ConcurrencyQuotaActionResult =
41+
| { ok: true }
42+
| {
43+
ok: false;
44+
errors: Record<string, string[] | undefined>;
45+
formError?: string;
46+
};
47+
48+
export async function handleConcurrencyQuotaAction(
49+
formData: FormData,
50+
orgId: string,
51+
adminUserId: string
52+
): Promise<ConcurrencyQuotaActionResult> {
53+
const submission = SetConcurrencyQuotaSchema.safeParse(
54+
Object.fromEntries(formData)
55+
);
56+
if (!submission.success) {
57+
return { ok: false, errors: submission.error.flatten().fieldErrors };
58+
}
59+
60+
const result = await setExtraConcurrencyQuota(orgId, {
61+
extraConcurrencyQuota: submission.data.extraConcurrencyQuota,
62+
});
63+
64+
if (!result) {
65+
return {
66+
ok: false,
67+
errors: {},
68+
formError:
69+
"Billing client unavailable — check BILLING_API_URL/BILLING_API_KEY config.",
70+
};
71+
}
72+
73+
if (!result.success) {
74+
// The platform client's generic error path strips `code` to `error` only
75+
// until the BillingClient.fetch passthrough fix lands; cast keeps the
76+
// route forward-compatible so precise UI copy renders automatically once
77+
// it does.
78+
const err = result as {
79+
success: false;
80+
error: string;
81+
code?: string;
82+
};
83+
return {
84+
ok: false,
85+
errors: {},
86+
formError: mapCodeToMessage(err.code, err.error),
87+
};
88+
}
89+
90+
logger.info("admin.backOffice.concurrencyQuota", {
91+
adminUserId,
92+
orgId,
93+
next: submission.data.extraConcurrencyQuota,
94+
});
95+
96+
return { ok: true };
97+
}
98+
99+
function mapCodeToMessage(
100+
code: string | undefined,
101+
fallback: string
102+
): string {
103+
switch (code) {
104+
case "invalid_body":
105+
return "Quota must be a non-negative integer (or check 'Use plan default').";
106+
case "quota_too_high":
107+
// Cloud's `error` string embeds the actual ceiling, prefer it verbatim.
108+
return fallback || "Cap is too high.";
109+
case "org_not_found":
110+
return "Organization not found.";
111+
case "limits_not_found":
112+
return "This org has no billing limits row yet.";
113+
default:
114+
return fallback || "Failed to update concurrency quota.";
115+
}
116+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Form } from "@remix-run/react";
2+
import { useEffect, useState } from "react";
3+
import { Button } from "~/components/primitives/Buttons";
4+
import { Checkbox } from "~/components/primitives/Checkbox";
5+
import { FormError } from "~/components/primitives/FormError";
6+
import { Header2 } from "~/components/primitives/Headers";
7+
import { Input } from "~/components/primitives/Input";
8+
import { Label } from "~/components/primitives/Label";
9+
import { Paragraph } from "~/components/primitives/Paragraph";
10+
import * as Property from "~/components/primitives/PropertyTable";
11+
12+
export const CONCURRENCY_QUOTA_INTENT = "set-concurrency-quota";
13+
export const CONCURRENCY_QUOTA_SAVED_VALUE = "concurrency-quota";
14+
15+
type FieldErrors = Record<string, string[] | undefined> | null;
16+
17+
type Props = {
18+
currentQuota: number;
19+
purchased: number;
20+
errors: FieldErrors;
21+
formError: string | null;
22+
savedJustNow: boolean;
23+
isSubmitting: boolean;
24+
};
25+
26+
export function ConcurrencyQuotaSection({
27+
currentQuota,
28+
purchased,
29+
errors,
30+
formError,
31+
savedJustNow,
32+
isSubmitting,
33+
}: Props) {
34+
const hasFieldErrors = !!errors && Object.keys(errors).length > 0;
35+
const fieldError = (field: string) =>
36+
errors && field in errors ? errors[field]?.[0] : undefined;
37+
38+
const [isEditing, setIsEditing] = useState(hasFieldErrors || !!formError);
39+
const [usePlanDefault, setUsePlanDefault] = useState(false);
40+
const [value, setValue] = useState(String(currentQuota));
41+
42+
useEffect(() => {
43+
if (hasFieldErrors || formError) setIsEditing(true);
44+
}, [hasFieldErrors, formError]);
45+
46+
useEffect(() => {
47+
if (savedJustNow && !hasFieldErrors && !formError) setIsEditing(false);
48+
}, [savedJustNow, hasFieldErrors, formError]);
49+
50+
const cancelEdit = () => {
51+
setValue(String(currentQuota));
52+
setUsePlanDefault(false);
53+
setIsEditing(false);
54+
};
55+
56+
return (
57+
<section className="flex flex-col gap-3 rounded-md border border-charcoal-700 bg-charcoal-800 p-4">
58+
<div className="flex items-center justify-between">
59+
<Header2>Concurrency quota</Header2>
60+
{!isEditing && (
61+
<Button
62+
variant="tertiary/small"
63+
onClick={() => setIsEditing(true)}
64+
disabled={isSubmitting}
65+
>
66+
Edit
67+
</Button>
68+
)}
69+
</div>
70+
71+
<Paragraph variant="small">
72+
Cap on how much extra concurrency this org can purchase. Increases
73+
unlock self-serve purchase up to the new cap; the org still has to
74+
complete the purchase from the billing flow.
75+
</Paragraph>
76+
77+
{savedJustNow && (
78+
<div className="rounded-md border border-green-600/40 bg-green-600/10 px-3 py-2">
79+
<Paragraph variant="small" className="text-green-500">
80+
Saved.
81+
</Paragraph>
82+
</div>
83+
)}
84+
85+
{formError && (
86+
<div className="rounded-md border border-red-600/40 bg-red-600/10 px-3 py-2">
87+
<Paragraph variant="small" className="text-red-500">
88+
{formError}
89+
</Paragraph>
90+
</div>
91+
)}
92+
93+
{!isEditing ? (
94+
<Property.Table>
95+
<Property.Item>
96+
<Property.Label>Max extra concurrency this org can purchase on top of their plan</Property.Label>
97+
<Property.Value>{currentQuota.toLocaleString()}</Property.Value>
98+
</Property.Item>
99+
<Property.Item>
100+
<Property.Label>Already purchased</Property.Label>
101+
<Property.Value>{purchased.toLocaleString()}</Property.Value>
102+
</Property.Item>
103+
</Property.Table>
104+
) : (
105+
<Form method="post" className="flex flex-col gap-3 pt-2">
106+
<input type="hidden" name="intent" value={CONCURRENCY_QUOTA_INTENT} />
107+
108+
<div className="flex flex-col gap-1">
109+
<Label>Max extra concurrency this org can purchase on top of their plan</Label>
110+
<Input
111+
name="extraConcurrencyQuota"
112+
type="number"
113+
min={0}
114+
value={usePlanDefault ? "" : value}
115+
onChange={(e) => setValue(e.target.value)}
116+
disabled={usePlanDefault}
117+
required={!usePlanDefault}
118+
/>
119+
<FormError>{fieldError("extraConcurrencyQuota")}</FormError>
120+
</div>
121+
122+
<div className="flex items-center gap-2">
123+
<Checkbox
124+
id="usePlanDefault"
125+
name="usePlanDefault"
126+
value="true"
127+
checked={usePlanDefault}
128+
onChange={(e) => setUsePlanDefault(e.target.checked)}
129+
/>
130+
<Label htmlFor="usePlanDefault">
131+
Use plan default (clears any per-org override)
132+
</Label>
133+
</div>
134+
135+
<div className="flex items-center gap-2">
136+
<Button
137+
type="submit"
138+
variant="primary/medium"
139+
disabled={isSubmitting || (!usePlanDefault && !value.trim())}
140+
>
141+
Save
142+
</Button>
143+
<Button
144+
type="button"
145+
variant="tertiary/medium"
146+
onClick={cancelEdit}
147+
disabled={isSubmitting}
148+
>
149+
Cancel
150+
</Button>
151+
</div>
152+
</Form>
153+
)}
154+
</section>
155+
);
156+
}

0 commit comments

Comments
 (0)