keyRateLimitsForm.tsx 8.1 KB

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