import { RATE_LIMIT_WINDOW_REGEX } from '@documenso/lib/types/subscription'; import { Button } from '@documenso/ui/primitives/button'; import { Input } from '@documenso/ui/primitives/input'; import { Trans, useLingui } from '@lingui/react/macro'; import { PlusIcon, Trash2Icon } from 'lucide-react'; import { useState } from 'react'; type RateLimitEntryValue = { window: string; max: number }; type RateLimitArrayInputProps = { value: RateLimitEntryValue[]; onChange: (value: RateLimitEntryValue[]) => void; disabled?: boolean; }; const EMPTY_ENTRY: RateLimitEntryValue = { window: '', max: 0 }; /** A row counts as "started" once either field has input; fully-empty rows are dropped on commit. */ const hasEntryInput = (entry: RateLimitEntryValue) => entry.window.trim() !== '' || entry.max > 0; /** Keep in-progress rows; drop rows that are completely empty. */ const persistEntries = (entries: RateLimitEntryValue[]) => { return entries.map((entry) => ({ ...entry, window: entry.window.trim() })).filter(hasEntryInput); }; export const RateLimitArrayInput = ({ value, onChange, disabled }: RateLimitArrayInputProps) => { const { t } = useLingui(); const [draftEntry, setDraftEntry] = useState(null); const entries = draftEntry ? [...value, draftEntry] : value.length ? value : [EMPTY_ENTRY]; const getWindowError = (entry: RateLimitEntryValue, index: number) => { const window = entry.window.trim(); if (!hasEntryInput(entry)) { return null; } if (window === '') { return t`Enter a window, e.g. 5m`; } if (!RATE_LIMIT_WINDOW_REGEX.test(window)) { return t`Use a duration with a unit, e.g. 5m, 1h, or 24h`; } const isDuplicateWindow = entries.some((otherEntry, otherIndex) => { return otherIndex !== index && otherEntry.window.trim() === window; }); return isDuplicateWindow ? t`Use a unique window for each rate limit` : null; }; const getMaxError = (entry: RateLimitEntryValue) => { if (!hasEntryInput(entry)) { return null; } return entry.max > 0 ? null : t`Enter a max request count greater than 0`; }; const updateEntry = (index: number, patch: Partial) => { if (index >= value.length) { const nextDraftEntry = { ...(draftEntry ?? EMPTY_ENTRY), ...patch }; if (hasEntryInput(nextDraftEntry)) { onChange(persistEntries([...value, nextDraftEntry])); setDraftEntry(null); return; } setDraftEntry(nextDraftEntry); return; } const next = value.map((entry, i) => (i === index ? { ...entry, ...patch } : entry)); onChange(persistEntries(next)); }; const removeEntry = (index: number) => { if (index >= value.length) { setDraftEntry(null); return; } const next = value.filter((_, i) => i !== index); onChange(persistEntries(next)); }; const addEntry = () => { setDraftEntry(EMPTY_ENTRY); }; const hasErrors = entries.some((entry, index) => getWindowError(entry, index) || getMaxError(entry)); const isAddDisabled = disabled || value.length === 0 || Boolean(draftEntry) || hasErrors; return (
Window Max requests
{entries.map((entry, index) => { const windowError = getWindowError(entry, index); const maxError = getMaxError(entry); return (
updateEntry(index, { window: e.target.value })} /> updateEntry(index, { max: parseInt(e.target.value, 10) || 0 })} />
{windowError ?

{windowError}

: null} {maxError ?

{maxError}

: null}
); })}
); };