keyRateLimitsForm.tsx 8.0 KB

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