keyRateLimitsForm.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import type {RouteComponentProps} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import sortBy from 'lodash/sortBy';
  4. import Feature from 'sentry/components/acl/feature';
  5. import FeatureDisabled from 'sentry/components/acl/featureDisabled';
  6. import RangeSlider from 'sentry/components/forms/controls/rangeSlider';
  7. import Form from 'sentry/components/forms/form';
  8. import FormField from 'sentry/components/forms/formField';
  9. import InputControl from 'sentry/components/input';
  10. import Panel from 'sentry/components/panels/panel';
  11. import PanelAlert from 'sentry/components/panels/panelAlert';
  12. import PanelBody from 'sentry/components/panels/panelBody';
  13. import PanelHeader from 'sentry/components/panels/panelHeader';
  14. import {t, tct, tn} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Organization} from 'sentry/types/organization';
  17. import type {ProjectKey} from 'sentry/types/project';
  18. import {defined} from 'sentry/utils';
  19. import {getExactDuration} from 'sentry/utils/duration/getExactDuration';
  20. const PREDEFINED_RATE_LIMIT_VALUES = [
  21. 0, 60, 300, 900, 3600, 7200, 14400, 21600, 43200, 86400,
  22. ];
  23. type RateLimitValue = {
  24. count: number;
  25. window: number;
  26. };
  27. type Props = {
  28. data: ProjectKey;
  29. disabled: boolean;
  30. organization: Organization;
  31. } & Pick<
  32. RouteComponentProps<
  33. {
  34. keyId: string;
  35. projectId: string;
  36. },
  37. {}
  38. >,
  39. 'params'
  40. >;
  41. function KeyRateLimitsForm({data, disabled, organization, params}: Props) {
  42. function handleChangeWindow(
  43. onChange: (value: RateLimitValue, event: React.ChangeEvent<HTMLInputElement>) => void,
  44. onBlur: (value: RateLimitValue, event: React.ChangeEvent<HTMLInputElement>) => void,
  45. currentValueObj: RateLimitValue,
  46. value: number,
  47. event: React.ChangeEvent<HTMLInputElement>
  48. ) {
  49. const valueObj = {...currentValueObj, window: value};
  50. onChange(valueObj, event);
  51. onBlur(valueObj, event);
  52. }
  53. function handleChangeCount(
  54. callback: (value: RateLimitValue, event: React.ChangeEvent<HTMLInputElement>) => void,
  55. value: RateLimitValue,
  56. event: React.ChangeEvent<HTMLInputElement>
  57. ) {
  58. const valueObj = {
  59. ...value,
  60. count: Number(event.target.value),
  61. };
  62. callback(valueObj, event);
  63. }
  64. function getAllowedRateLimitValues(currentRateLimit?: number) {
  65. const {rateLimit} = data;
  66. const {window} = rateLimit ?? {};
  67. // The slider should display other values if they are set via the API, but still offer to select only the predefined values
  68. if (defined(window)) {
  69. // If the API returns a value not found in the predefined values and the user selects another value through the UI,
  70. // he will no longer be able to reselect the "custom" value in the slider
  71. if (currentRateLimit !== window) {
  72. return PREDEFINED_RATE_LIMIT_VALUES;
  73. }
  74. // If the API returns a value not found in the predefined values, that value will be added to the slider
  75. if (!PREDEFINED_RATE_LIMIT_VALUES.includes(window)) {
  76. return sortBy([...PREDEFINED_RATE_LIMIT_VALUES, window]);
  77. }
  78. }
  79. return PREDEFINED_RATE_LIMIT_VALUES;
  80. }
  81. const {keyId, projectId} = params;
  82. const apiEndpoint = `/projects/${organization.slug}/${projectId}/keys/${keyId}/`;
  83. const disabledAlert = ({features}) => (
  84. <FeatureDisabled
  85. alert={PanelAlert}
  86. features={features}
  87. featureName={t('Key Rate Limits')}
  88. />
  89. );
  90. return (
  91. <Form saveOnBlur apiEndpoint={apiEndpoint} apiMethod="PUT" initialData={data}>
  92. <Feature
  93. features="projects:rate-limits"
  94. hookName="feature-disabled:rate-limits"
  95. renderDisabled={({children, ...props}) =>
  96. typeof children === 'function' &&
  97. children({...props, renderDisabled: disabledAlert})
  98. }
  99. >
  100. {({hasFeature, features, project, renderDisabled}) => (
  101. <Panel>
  102. <PanelHeader>{t('Rate Limits')}</PanelHeader>
  103. <PanelBody>
  104. <PanelAlert type="info" showIcon>
  105. {t(
  106. `Rate limits provide a flexible way to manage your error
  107. volume. If you have a noisy project or environment you
  108. can configure a rate limit for this key to reduce the
  109. number of errors processed. To manage your transaction
  110. volume, we recommend adjusting your sample rate in your
  111. SDK configuration.`
  112. )}
  113. </PanelAlert>
  114. {!hasFeature &&
  115. typeof renderDisabled === 'function' &&
  116. renderDisabled({
  117. organization,
  118. project,
  119. features,
  120. hasFeature,
  121. children: null,
  122. })}
  123. <FormField
  124. name="rateLimit"
  125. label={t('Rate Limit')}
  126. disabled={disabled || !hasFeature}
  127. validate={({form}) => {
  128. // TODO(TS): is validate actually doing anything because it's an unexpected prop
  129. const isValid =
  130. form?.rateLimit &&
  131. typeof form.rateLimit.count !== 'undefined' &&
  132. typeof form.rateLimit.window !== 'undefined';
  133. if (isValid) {
  134. return [];
  135. }
  136. return [['rateLimit', t('Fill in both fields first')]];
  137. }}
  138. formatMessageValue={({count, window}: RateLimitValue) =>
  139. tct('[errors] in [timeWindow]', {
  140. errors: tn('%s error ', '%s errors ', count),
  141. timeWindow:
  142. window === 0 ? t('no time window') : getExactDuration(window),
  143. })
  144. }
  145. help={t(
  146. 'Apply a rate limit to this credential to cap the amount of errors accepted during a time window.'
  147. )}
  148. inline={false}
  149. >
  150. {({onChange, onBlur, value}) => {
  151. const window = typeof value === 'object' ? value.window : undefined;
  152. return (
  153. <RateLimitRow>
  154. <InputControl
  155. type="number"
  156. name="rateLimit.count"
  157. min={0}
  158. value={typeof value === 'object' ? value.count : undefined}
  159. placeholder={t('Count')}
  160. disabled={disabled || !hasFeature}
  161. onChange={event => handleChangeCount(onChange, value, event)}
  162. onBlur={event => handleChangeCount(onBlur, value, event)}
  163. />
  164. <EventsIn>{t('event(s) in')}</EventsIn>
  165. <RangeSlider
  166. name="rateLimit.window"
  167. allowedValues={getAllowedRateLimitValues(window)}
  168. value={window}
  169. placeholder={t('Window')}
  170. formatLabel={rangeValue => {
  171. if (typeof rangeValue === 'number') {
  172. if (rangeValue === 0) {
  173. return t('None');
  174. }
  175. return getExactDuration(rangeValue);
  176. }
  177. return undefined;
  178. }}
  179. disabled={disabled || !hasFeature}
  180. onChange={(rangeValue, event) =>
  181. handleChangeWindow(
  182. onChange,
  183. onBlur,
  184. value,
  185. Number(rangeValue),
  186. event
  187. )
  188. }
  189. />
  190. </RateLimitRow>
  191. );
  192. }}
  193. </FormField>
  194. </PanelBody>
  195. </Panel>
  196. )}
  197. </Feature>
  198. </Form>
  199. );
  200. }
  201. export default KeyRateLimitsForm;
  202. const RateLimitRow = styled('div')`
  203. display: grid;
  204. grid-template-columns: 2fr 1fr 2fr;
  205. align-items: center;
  206. gap: ${space(1)};
  207. `;
  208. const EventsIn = styled('small')`
  209. font-size: ${p => p.theme.fontSizeRelativeSmall};
  210. text-align: center;
  211. white-space: nowrap;
  212. `;