import type {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import sortBy from 'lodash/sortBy'; import Feature from 'sentry/components/acl/feature'; import FeatureDisabled from 'sentry/components/acl/featureDisabled'; import RangeSlider from 'sentry/components/forms/controls/rangeSlider'; import Form from 'sentry/components/forms/form'; import FormField from 'sentry/components/forms/formField'; import InputControl from 'sentry/components/input'; import Panel from 'sentry/components/panels/panel'; import PanelAlert from 'sentry/components/panels/panelAlert'; import PanelBody from 'sentry/components/panels/panelBody'; import PanelHeader from 'sentry/components/panels/panelHeader'; import {t, tct, tn} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import type {ProjectKey} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import {getExactDuration} from 'sentry/utils/duration/getExactDuration'; const PREDEFINED_RATE_LIMIT_VALUES = [ 0, 60, 300, 900, 3600, 7200, 14400, 21600, 43200, 86400, ]; type RateLimitValue = { count: number; window: number; }; type Props = { data: ProjectKey; disabled: boolean; organization: Organization; } & Pick< RouteComponentProps< { keyId: string; projectId: string; }, {} >, 'params' >; function KeyRateLimitsForm({data, disabled, organization, params}: Props) { function handleChangeWindow( onChange: (value: RateLimitValue, event: React.ChangeEvent) => void, onBlur: (value: RateLimitValue, event: React.ChangeEvent) => void, currentValueObj: RateLimitValue, value: number, event: React.ChangeEvent ) { const valueObj = {...currentValueObj, window: value}; onChange(valueObj, event); onBlur(valueObj, event); } function handleChangeCount( callback: (value: RateLimitValue, event: React.ChangeEvent) => void, value: RateLimitValue, event: React.ChangeEvent ) { const valueObj = { ...value, count: Number(event.target.value), }; callback(valueObj, event); } function getAllowedRateLimitValues(currentRateLimit?: number) { const {rateLimit} = data; const {window} = rateLimit ?? {}; // The slider should display other values if they are set via the API, but still offer to select only the predefined values if (defined(window)) { // If the API returns a value not found in the predefined values and the user selects another value through the UI, // he will no longer be able to reselect the "custom" value in the slider if (currentRateLimit !== window) { return PREDEFINED_RATE_LIMIT_VALUES; } // If the API returns a value not found in the predefined values, that value will be added to the slider if (!PREDEFINED_RATE_LIMIT_VALUES.includes(window)) { return sortBy([...PREDEFINED_RATE_LIMIT_VALUES, window]); } } return PREDEFINED_RATE_LIMIT_VALUES; } const {keyId, projectId} = params; const apiEndpoint = `/projects/${organization.slug}/${projectId}/keys/${keyId}/`; const disabledAlert = ({features}) => ( ); return (
typeof children === 'function' && children({...props, renderDisabled: disabledAlert}) } > {({hasFeature, features, project, renderDisabled}) => ( {t('Rate Limits')} {t( `Rate limits provide a flexible way to manage your error volume. If you have a noisy project or environment you can configure a rate limit for this key to reduce the number of errors processed. To manage your transaction volume, we recommend adjusting your sample rate in your SDK configuration.` )} {!hasFeature && typeof renderDisabled === 'function' && renderDisabled({ organization, project, features, hasFeature, children: null, })} { // TODO(TS): is validate actually doing anything because it's an unexpected prop const isValid = form?.rateLimit && typeof form.rateLimit.count !== 'undefined' && typeof form.rateLimit.window !== 'undefined'; if (isValid) { return []; } return [['rateLimit', t('Fill in both fields first')]]; }} formatMessageValue={({count, window}: RateLimitValue) => tct('[errors] in [timeWindow]', { errors: tn('%s error ', '%s errors ', count), timeWindow: window === 0 ? t('no time window') : getExactDuration(window), }) } help={t( 'Apply a rate limit to this credential to cap the amount of errors accepted during a time window.' )} inline={false} > {({onChange, onBlur, value}) => { const window = typeof value === 'object' ? value.window : undefined; return ( handleChangeCount(onChange, value, event)} onBlur={event => handleChangeCount(onBlur, value, event)} /> {t('event(s) in')} { if (typeof rangeValue === 'number') { if (rangeValue === 0) { return t('None'); } return getExactDuration(rangeValue); } return undefined; }} disabled={disabled || !hasFeature} onChange={(rangeValue, event) => handleChangeWindow( onChange, onBlur, value, Number(rangeValue), event ) } /> ); }} )}
); } export default KeyRateLimitsForm; const RateLimitRow = styled('div')` display: grid; grid-template-columns: 2fr 1fr 2fr; align-items: center; gap: ${space(1)}; `; const EventsIn = styled('small')` font-size: ${p => p.theme.fontSizeRelativeSmall}; text-align: center; white-space: nowrap; `;