From 8403d6cdca770df818cedd12aa24f266c9f87107 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Mon, 22 Jun 2026 17:50:49 +0300 Subject: [PATCH] fix: admin organisation limits and usage UI --- .../components/general/claim-limit-fields.tsx | 89 +++--- .../organisation-usage-reset-button.tsx | 2 + .../admin+/organisations.$id.tsx | 270 +++++++++--------- .../rate-limit/compute-quota-flags.ts | 37 +-- .../rate-limit/get-quota-alert-kind.ts | 4 +- packages/lib/types/subscription.ts | 39 ++- 6 files changed, 222 insertions(+), 219 deletions(-) diff --git a/apps/remix/app/components/general/claim-limit-fields.tsx b/apps/remix/app/components/general/claim-limit-fields.tsx index ed29c8fc3..494f18a0f 100644 --- a/apps/remix/app/components/general/claim-limit-fields.tsx +++ b/apps/remix/app/components/general/claim-limit-fields.tsx @@ -1,11 +1,4 @@ -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@documenso/ui/primitives/form/form'; +import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { Trans, useLingui } from '@lingui/react/macro'; import type { ReactNode } from 'react'; @@ -20,6 +13,12 @@ type ClaimLimitFieldsProps = { disabled?: boolean; }; +type LimitGroup = { + title: ReactNode; + quotaKey: string; + rateLimitKey: string; +}; + export const ClaimLimitFields = ({ control, prefix = '', @@ -30,13 +29,33 @@ export const ClaimLimitFields = ({ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const name = (key: string) => `${prefix}${key}` as Path; - const renderQuotaField = (key: string, label: ReactNode, description: ReactNode) => ( + const limitGroups: LimitGroup[] = [ + { + title: Documents, + quotaKey: 'documentQuota', + rateLimitKey: 'documentRateLimits', + }, + { + title: Emails, + quotaKey: 'emailQuota', + rateLimitKey: 'emailRateLimits', + }, + { + title: API, + quotaKey: 'apiQuota', + rateLimitKey: 'apiRateLimits', + }, + ]; + + const renderQuotaField = (group: LimitGroup) => ( ( - {label} + + Monthly quota + ({ onChange={(e) => field.onChange(e.target.value === '' ? null : parseInt(e.target.value, 10))} /> - {description} )} /> ); - const renderRateLimitField = (key: string, label: ReactNode) => ( + const renderRateLimitField = (group: LimitGroup) => ( ( - {label} - )} /> ); return ( -
- - Limits - +
+
+

+ Limits +

+

+ + Empty quota means unlimited, 0 blocks the resource. Rate limit windows accept values like 5m, 1h or 24h. + +

+
- {renderQuotaField( - 'documentQuota', - Monthly document quota, - Empty = Unlimited, 0 = Blocked, - )} - {renderRateLimitField('documentRateLimits', Document rate limits)} +
+
+ {limitGroups.map((group) => ( +
+

{group.title}

- {renderQuotaField( - 'emailQuota', - Monthly email quota, - Empty = Unlimited, 0 = Blocked, - )} - {renderRateLimitField('emailRateLimits', Email rate limits)} - - {renderQuotaField('apiQuota', Monthly API quota, Empty = Unlimited, 0 = Blocked)} - {renderRateLimitField('apiRateLimits', API rate limits)} + {renderQuotaField(group)} + {renderRateLimitField(group)} +
+ ))} +
+
); }; diff --git a/apps/remix/app/components/general/organisation-usage-reset-button.tsx b/apps/remix/app/components/general/organisation-usage-reset-button.tsx index 7451f6e1c..bec8b8dd6 100644 --- a/apps/remix/app/components/general/organisation-usage-reset-button.tsx +++ b/apps/remix/app/components/general/organisation-usage-reset-button.tsx @@ -2,6 +2,7 @@ import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { Trans, useLingui } from '@lingui/react/macro'; +import { RotateCcwIcon } from 'lucide-react'; import { useRevalidator } from 'react-router'; type OrganisationUsageResetButtonProps = { @@ -32,6 +33,7 @@ export const OrganisationUsageResetButton = ({ organisationId, counter }: Organi loading={isPending} onClick={() => reset({ organisationId, counter })} > + Reset ); diff --git a/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx b/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx index ac668ee3f..10b346083 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx @@ -42,7 +42,6 @@ import { AdminOrganisationDeleteDialog } from '~/components/dialogs/admin-organi import { AdminOrganisationMemberDeleteDialog } from '~/components/dialogs/admin-organisation-member-delete-dialog'; import { AdminOrganisationMemberUpdateDialog } from '~/components/dialogs/admin-organisation-member-update-dialog'; import { AdminOrganisationSyncSubscriptionDialog } from '~/components/dialogs/admin-organisation-sync-subscription-dialog'; -import { DetailsCard, DetailsValue } from '~/components/general/admin-details'; import { AdminGlobalSettingsSection } from '~/components/general/admin-global-settings-section'; import { ClaimLimitFields } from '~/components/general/claim-limit-fields'; import { GenericErrorLayout } from '~/components/general/generic-error-layout'; @@ -268,54 +267,32 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro -
-
-
-

- Organisation usage -

-

- Current usage against organisation limits. -

-
-
+ -
- Members}> - - {organisation.members.length} /{' '} - {organisation.organisationClaim.memberCount === 0 - ? t`Unlimited` - : organisation.organisationClaim.memberCount} - - - - Teams}> - - {organisation.teams.length} /{' '} - {organisation.organisationClaim.teamCount === 0 ? t`Unlimited` : organisation.organisationClaim.teamCount} - - -
- -
- -
-
+
-

+

Global Settings

-

+

Default settings applied to this organisation.

@@ -406,21 +383,27 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro
-