Skip to content

Commit 147195b

Browse files
committed
feat(webapp): admin editor for org batch rate limit
1 parent 3cc7248 commit 147195b

4 files changed

Lines changed: 141 additions & 20 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Admin back office: edit an organization's batch rate limit (`batchRateLimitConfig`) from the org page, alongside the existing API rate limit editor. The rate-limit form UI is now shared between the API and batch sections.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { prisma } from "~/db.server";
2+
import { env } from "~/env.server";
3+
import { logger } from "~/services/logger.server";
4+
import { type Duration } from "~/services/rateLimiter.server";
5+
import { BATCH_RATE_LIMIT_INTENT } from "./BatchRateLimitSection";
6+
import {
7+
handleRateLimitAction,
8+
resolveEffectiveRateLimit,
9+
type RateLimitActionResult,
10+
type RateLimitDomain,
11+
} from "./RateLimitSection.server";
12+
import type { EffectiveRateLimit } from "./RateLimitSection";
13+
14+
export const batchRateLimitDomain: RateLimitDomain = {
15+
intent: BATCH_RATE_LIMIT_INTENT,
16+
systemDefault: () => ({
17+
type: "tokenBucket",
18+
refillRate: env.BATCH_RATE_LIMIT_REFILL_RATE,
19+
interval: env.BATCH_RATE_LIMIT_REFILL_INTERVAL as Duration,
20+
maxTokens: env.BATCH_RATE_LIMIT_MAX,
21+
}),
22+
apply: async (orgId, next, adminUserId) => {
23+
const existing = await prisma.organization.findFirst({
24+
where: { id: orgId },
25+
select: { batchRateLimitConfig: true },
26+
});
27+
if (!existing) {
28+
throw new Response(null, { status: 404 });
29+
}
30+
await prisma.organization.update({
31+
where: { id: orgId },
32+
data: { batchRateLimitConfig: next as any },
33+
});
34+
logger.info("admin.backOffice.batchRateLimit", {
35+
adminUserId,
36+
orgId,
37+
previous: existing.batchRateLimitConfig,
38+
next,
39+
});
40+
},
41+
};
42+
43+
export function resolveEffectiveBatchRateLimit(
44+
override: unknown
45+
): EffectiveRateLimit {
46+
return resolveEffectiveRateLimit(override, batchRateLimitDomain);
47+
}
48+
49+
export function handleBatchRateLimitAction(
50+
formData: FormData,
51+
orgId: string,
52+
adminUserId: string
53+
): Promise<RateLimitActionResult> {
54+
return handleRateLimitAction(formData, orgId, adminUserId, batchRateLimitDomain);
55+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {
2+
RateLimitSection,
3+
type EffectiveRateLimit,
4+
} from "./RateLimitSection";
5+
6+
export const BATCH_RATE_LIMIT_INTENT = "set-batch-rate-limit";
7+
export const BATCH_RATE_LIMIT_SAVED_VALUE = "batch-rate-limit";
8+
9+
type FieldErrors = Record<string, string[] | undefined> | null;
10+
11+
type Props = {
12+
effective: EffectiveRateLimit;
13+
errors: FieldErrors;
14+
savedJustNow: boolean;
15+
isSubmitting: boolean;
16+
};
17+
18+
export function BatchRateLimitSection(props: Props) {
19+
return (
20+
<RateLimitSection
21+
title="Batch rate limit"
22+
intent={BATCH_RATE_LIMIT_INTENT}
23+
{...props}
24+
/>
25+
);
26+
}

apps/webapp/app/routes/admin.back-office.orgs.$orgId.tsx

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,30 @@ import {
77
useTypedActionData,
88
useTypedLoaderData,
99
} from "remix-typedjson";
10+
import {
11+
API_RATE_LIMIT_INTENT,
12+
API_RATE_LIMIT_SAVED_VALUE,
13+
ApiRateLimitSection,
14+
} from "~/components/admin/backOffice/ApiRateLimitSection";
15+
import {
16+
handleApiRateLimitAction,
17+
resolveEffectiveApiRateLimit,
18+
} from "~/components/admin/backOffice/ApiRateLimitSection.server";
19+
import {
20+
BATCH_RATE_LIMIT_INTENT,
21+
BATCH_RATE_LIMIT_SAVED_VALUE,
22+
BatchRateLimitSection,
23+
} from "~/components/admin/backOffice/BatchRateLimitSection";
24+
import {
25+
handleBatchRateLimitAction,
26+
resolveEffectiveBatchRateLimit,
27+
} from "~/components/admin/backOffice/BatchRateLimitSection.server";
1028
import {
1129
MAX_PROJECTS_INTENT,
1230
MAX_PROJECTS_SAVED_VALUE,
1331
MaxProjectsSection,
1432
} from "~/components/admin/backOffice/MaxProjectsSection";
1533
import { handleMaxProjectsAction } from "~/components/admin/backOffice/MaxProjectsSection.server";
16-
import {
17-
RATE_LIMIT_INTENT,
18-
RATE_LIMIT_SAVED_VALUE,
19-
RateLimitSection,
20-
} from "~/components/admin/backOffice/RateLimitSection";
21-
import {
22-
handleRateLimitAction,
23-
resolveEffectiveRateLimit,
24-
} from "~/components/admin/backOffice/RateLimitSection.server";
2534
import { LinkButton } from "~/components/primitives/Buttons";
2635
import { CopyableText } from "~/components/primitives/CopyableText";
2736
import { Header1 } from "~/components/primitives/Headers";
@@ -50,6 +59,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
5059
title: true,
5160
createdAt: true,
5261
apiRateLimiterConfig: true,
62+
batchRateLimitConfig: true,
5363
maximumProjectCount: true,
5464
},
5565
});
@@ -58,9 +68,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
5868
throw new Response(null, { status: 404 });
5969
}
6070

61-
const effective = resolveEffectiveRateLimit(org.apiRateLimiterConfig);
71+
const apiEffective = resolveEffectiveApiRateLimit(org.apiRateLimiterConfig);
72+
const batchEffective = resolveEffectiveBatchRateLimit(
73+
org.batchRateLimitConfig
74+
);
6275

63-
return typedjson({ org, effective });
76+
return typedjson({ org, apiEffective, batchEffective });
6477
}
6578

6679
export async function action({ request, params }: ActionFunctionArgs) {
@@ -90,16 +103,29 @@ export async function action({ request, params }: ActionFunctionArgs) {
90103
);
91104
}
92105

93-
if (intent === RATE_LIMIT_INTENT) {
94-
const result = await handleRateLimitAction(formData, orgId, user.id);
106+
if (intent === API_RATE_LIMIT_INTENT) {
107+
const result = await handleApiRateLimitAction(formData, orgId, user.id);
95108
if (!result.ok) {
96109
return typedjson(
97-
{ section: RATE_LIMIT_SAVED_VALUE, errors: result.errors },
110+
{ section: API_RATE_LIMIT_SAVED_VALUE, errors: result.errors },
98111
{ status: 400 }
99112
);
100113
}
101114
return redirect(
102-
`/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${RATE_LIMIT_SAVED_VALUE}`
115+
`/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${API_RATE_LIMIT_SAVED_VALUE}`
116+
);
117+
}
118+
119+
if (intent === BATCH_RATE_LIMIT_INTENT) {
120+
const result = await handleBatchRateLimitAction(formData, orgId, user.id);
121+
if (!result.ok) {
122+
return typedjson(
123+
{ section: BATCH_RATE_LIMIT_SAVED_VALUE, errors: result.errors },
124+
{ status: 400 }
125+
);
126+
}
127+
return redirect(
128+
`/admin/back-office/orgs/${orgId}?${SAVED_QUERY_KEY}=${BATCH_RATE_LIMIT_SAVED_VALUE}`
103129
);
104130
}
105131

@@ -110,7 +136,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
110136
}
111137

112138
export default function BackOfficeOrgPage() {
113-
const { org, effective } = useTypedLoaderData<typeof loader>();
139+
const { org, apiEffective, batchEffective } =
140+
useTypedLoaderData<typeof loader>();
114141
const actionData = useTypedActionData<typeof action>();
115142
const navigation = useNavigation();
116143
const isSubmitting = navigation.state !== "idle";
@@ -154,10 +181,17 @@ export default function BackOfficeOrgPage() {
154181
</LinkButton>
155182
</div>
156183

157-
<RateLimitSection
158-
effective={effective}
159-
errors={errorSection === RATE_LIMIT_SAVED_VALUE ? errors : null}
160-
savedJustNow={savedSection === RATE_LIMIT_SAVED_VALUE}
184+
<ApiRateLimitSection
185+
effective={apiEffective}
186+
errors={errorSection === API_RATE_LIMIT_SAVED_VALUE ? errors : null}
187+
savedJustNow={savedSection === API_RATE_LIMIT_SAVED_VALUE}
188+
isSubmitting={isSubmitting}
189+
/>
190+
191+
<BatchRateLimitSection
192+
effective={batchEffective}
193+
errors={errorSection === BATCH_RATE_LIMIT_SAVED_VALUE ? errors : null}
194+
savedJustNow={savedSection === BATCH_RATE_LIMIT_SAVED_VALUE}
161195
isSubmitting={isSubmitting}
162196
/>
163197

0 commit comments

Comments
 (0)