123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- 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<HTMLInputElement>) => void,
- onBlur: (value: RateLimitValue, event: React.ChangeEvent<HTMLInputElement>) => void,
- currentValueObj: RateLimitValue,
- value: number,
- event: React.ChangeEvent<HTMLInputElement>
- ) {
- const valueObj = {...currentValueObj, window: value};
- onChange(valueObj, event);
- onBlur(valueObj, event);
- }
- function handleChangeCount(
- callback: (value: RateLimitValue, event: React.ChangeEvent<HTMLInputElement>) => void,
- value: RateLimitValue,
- event: React.ChangeEvent<HTMLInputElement>
- ) {
- 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}) => (
- <FeatureDisabled
- alert={PanelAlert}
- features={features}
- featureName={t('Key Rate Limits')}
- />
- );
- return (
- <Form saveOnBlur apiEndpoint={apiEndpoint} apiMethod="PUT" initialData={data}>
- <Feature
- features="projects:rate-limits"
- hookName="feature-disabled:rate-limits"
- renderDisabled={({children, ...props}) =>
- typeof children === 'function' &&
- children({...props, renderDisabled: disabledAlert})
- }
- >
- {({hasFeature, features, project, renderDisabled}) => (
- <Panel>
- <PanelHeader>{t('Rate Limits')}</PanelHeader>
- <PanelBody>
- <PanelAlert type="info" showIcon>
- {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.`
- )}
- </PanelAlert>
- {!hasFeature &&
- typeof renderDisabled === 'function' &&
- renderDisabled({
- organization,
- project,
- features,
- hasFeature,
- children: null,
- })}
- <FormField
- name="rateLimit"
- label={t('Rate Limit')}
- disabled={disabled || !hasFeature}
- validate={({form}) => {
- // 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 (
- <RateLimitRow>
- <InputControl
- type="number"
- name="rateLimit.count"
- min={0}
- value={typeof value === 'object' ? value.count : undefined}
- placeholder={t('Count')}
- disabled={disabled || !hasFeature}
- onChange={event => handleChangeCount(onChange, value, event)}
- onBlur={event => handleChangeCount(onBlur, value, event)}
- />
- <EventsIn>{t('event(s) in')}</EventsIn>
- <RangeSlider
- name="rateLimit.window"
- allowedValues={getAllowedRateLimitValues(window)}
- value={window}
- placeholder={t('Window')}
- formatLabel={rangeValue => {
- 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
- )
- }
- />
- </RateLimitRow>
- );
- }}
- </FormField>
- </PanelBody>
- </Panel>
- )}
- </Feature>
- </Form>
- );
- }
- 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;
- `;
|