keyRateLimitsForm.tsx 8.1 KB

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