keyRateLimitsForm.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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 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 {Organization} from 'sentry/types';
  17. import {defined} from 'sentry/utils';
  18. import {getExactDuration} from 'sentry/utils/formatters';
  19. import {ProjectKey} from 'sentry/views/settings/project/projectKeys/types';
  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 &&
  131. form.rateLimit &&
  132. typeof form.rateLimit.count !== 'undefined' &&
  133. typeof form.rateLimit.window !== 'undefined';
  134. if (isValid) {
  135. return [];
  136. }
  137. return [['rateLimit', t('Fill in both fields first')]];
  138. }}
  139. formatMessageValue={({count, window}: RateLimitValue) =>
  140. tct('[errors] in [timeWindow]', {
  141. errors: tn('%s error ', '%s errors ', count),
  142. timeWindow:
  143. window === 0 ? t('no time window') : getExactDuration(window),
  144. })
  145. }
  146. help={t(
  147. 'Apply a rate limit to this credential to cap the amount of errors accepted during a time window.'
  148. )}
  149. inline={false}
  150. >
  151. {({onChange, onBlur, value}) => {
  152. const window = typeof value === 'object' ? value.window : undefined;
  153. return (
  154. <RateLimitRow>
  155. <InputControl
  156. type="number"
  157. name="rateLimit.count"
  158. min={0}
  159. value={typeof value === 'object' ? value.count : undefined}
  160. placeholder={t('Count')}
  161. disabled={disabled || !hasFeature}
  162. onChange={event => handleChangeCount(onChange, value, event)}
  163. onBlur={event => handleChangeCount(onBlur, value, event)}
  164. />
  165. <EventsIn>{t('event(s) in')}</EventsIn>
  166. <RangeSlider
  167. name="rateLimit.window"
  168. allowedValues={getAllowedRateLimitValues(window)}
  169. value={window}
  170. placeholder={t('Window')}
  171. formatLabel={rangeValue => {
  172. if (typeof rangeValue === 'number') {
  173. if (rangeValue === 0) {
  174. return t('None');
  175. }
  176. return getExactDuration(rangeValue);
  177. }
  178. return undefined;
  179. }}
  180. disabled={disabled || !hasFeature}
  181. onChange={(rangeValue, event) =>
  182. handleChangeWindow(
  183. onChange,
  184. onBlur,
  185. value,
  186. Number(rangeValue),
  187. event
  188. )
  189. }
  190. />
  191. </RateLimitRow>
  192. );
  193. }}
  194. </FormField>
  195. </PanelBody>
  196. </Panel>
  197. )}
  198. </Feature>
  199. </Form>
  200. );
  201. }
  202. export default KeyRateLimitsForm;
  203. const RateLimitRow = styled('div')`
  204. display: grid;
  205. grid-template-columns: 2fr 1fr 2fr;
  206. align-items: center;
  207. gap: ${space(1)};
  208. `;
  209. const EventsIn = styled('small')`
  210. font-size: ${p => p.theme.fontSizeRelativeSmall};
  211. text-align: center;
  212. white-space: nowrap;
  213. `;