mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
233 lines
8.1 KiB
TypeScript
233 lines
8.1 KiB
TypeScript
import type {RouteComponentProps} from 'react-router';
|
|
import styled from '@emotion/styled';
|
|
import sortBy from 'lodash/sortBy';
|
|
|
|
import Feature from 'sentry/components/acl/feature';
|
|
import FeatureDisabled from 'sentry/components/acl/featureDisabled';
|
|
import RangeSlider from 'sentry/components/forms/controls/rangeSlider';
|
|
import Form from 'sentry/components/forms/form';
|
|
import FormField from 'sentry/components/forms/formField';
|
|
import InputControl from 'sentry/components/input';
|
|
import Panel from 'sentry/components/panels/panel';
|
|
import PanelAlert from 'sentry/components/panels/panelAlert';
|
|
import PanelBody from 'sentry/components/panels/panelBody';
|
|
import PanelHeader from 'sentry/components/panels/panelHeader';
|
|
import {t, tct, tn} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {Organization, ProjectKey} from 'sentry/types';
|
|
import {defined} from 'sentry/utils';
|
|
import {getExactDuration} from 'sentry/utils/formatters';
|
|
|
|
const PREDEFINED_RATE_LIMIT_VALUES = [
|
|
0, 60, 300, 900, 3600, 7200, 14400, 21600, 43200, 86400,
|
|
];
|
|
|
|
type RateLimitValue = {
|
|
count: number;
|
|
window: number;
|
|
};
|
|
|
|
type Props = {
|
|
data: ProjectKey;
|
|
disabled: boolean;
|
|
organization: Organization;
|
|
} & Pick<
|
|
RouteComponentProps<
|
|
{
|
|
keyId: string;
|
|
projectId: string;
|
|
},
|
|
{}
|
|
>,
|
|
'params'
|
|
>;
|
|
|
|
function KeyRateLimitsForm({data, disabled, organization, params}: Props) {
|
|
function handleChangeWindow(
|
|
onChange: (value: RateLimitValue, event: React.ChangeEvent<HTMLInputElement>) => void,
|
|
onBlur: (value: RateLimitValue, event: React.ChangeEvent<HTMLInputElement>) => void,
|
|
currentValueObj: RateLimitValue,
|
|
value: number,
|
|
event: React.ChangeEvent<HTMLInputElement>
|
|
) {
|
|
const valueObj = {...currentValueObj, window: value};
|
|
|
|
onChange(valueObj, event);
|
|
onBlur(valueObj, event);
|
|
}
|
|
|
|
function handleChangeCount(
|
|
callback: (value: RateLimitValue, event: React.ChangeEvent<HTMLInputElement>) => void,
|
|
value: RateLimitValue,
|
|
event: React.ChangeEvent<HTMLInputElement>
|
|
) {
|
|
const valueObj = {
|
|
...value,
|
|
count: Number(event.target.value),
|
|
};
|
|
|
|
callback(valueObj, event);
|
|
}
|
|
|
|
function getAllowedRateLimitValues(currentRateLimit?: number) {
|
|
const {rateLimit} = data;
|
|
const {window} = rateLimit ?? {};
|
|
|
|
// The slider should display other values if they are set via the API, but still offer to select only the predefined values
|
|
if (defined(window)) {
|
|
// If the API returns a value not found in the predefined values and the user selects another value through the UI,
|
|
// he will no longer be able to reselect the "custom" value in the slider
|
|
if (currentRateLimit !== window) {
|
|
return PREDEFINED_RATE_LIMIT_VALUES;
|
|
}
|
|
|
|
// If the API returns a value not found in the predefined values, that value will be added to the slider
|
|
if (!PREDEFINED_RATE_LIMIT_VALUES.includes(window)) {
|
|
return sortBy([...PREDEFINED_RATE_LIMIT_VALUES, window]);
|
|
}
|
|
}
|
|
|
|
return PREDEFINED_RATE_LIMIT_VALUES;
|
|
}
|
|
|
|
const {keyId, projectId} = params;
|
|
const apiEndpoint = `/projects/${organization.slug}/${projectId}/keys/${keyId}/`;
|
|
|
|
const disabledAlert = ({features}) => (
|
|
<FeatureDisabled
|
|
alert={PanelAlert}
|
|
features={features}
|
|
featureName={t('Key Rate Limits')}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<Form saveOnBlur apiEndpoint={apiEndpoint} apiMethod="PUT" initialData={data}>
|
|
<Feature
|
|
features="projects:rate-limits"
|
|
hookName="feature-disabled:rate-limits"
|
|
renderDisabled={({children, ...props}) =>
|
|
typeof children === 'function' &&
|
|
children({...props, renderDisabled: disabledAlert})
|
|
}
|
|
>
|
|
{({hasFeature, features, project, renderDisabled}) => (
|
|
<Panel>
|
|
<PanelHeader>{t('Rate Limits')}</PanelHeader>
|
|
|
|
<PanelBody>
|
|
<PanelAlert type="info" showIcon>
|
|
{t(
|
|
`Rate limits provide a flexible way to manage your error
|
|
volume. If you have a noisy project or environment you
|
|
can configure a rate limit for this key to reduce the
|
|
number of errors processed. To manage your transaction
|
|
volume, we recommend adjusting your sample rate in your
|
|
SDK configuration.`
|
|
)}
|
|
</PanelAlert>
|
|
{!hasFeature &&
|
|
typeof renderDisabled === 'function' &&
|
|
renderDisabled({
|
|
organization,
|
|
project,
|
|
features,
|
|
hasFeature,
|
|
children: null,
|
|
})}
|
|
<FormField
|
|
name="rateLimit"
|
|
label={t('Rate Limit')}
|
|
disabled={disabled || !hasFeature}
|
|
validate={({form}) => {
|
|
// TODO(TS): is validate actually doing anything because it's an unexpected prop
|
|
const isValid =
|
|
form?.rateLimit &&
|
|
typeof form.rateLimit.count !== 'undefined' &&
|
|
typeof form.rateLimit.window !== 'undefined';
|
|
|
|
if (isValid) {
|
|
return [];
|
|
}
|
|
|
|
return [['rateLimit', t('Fill in both fields first')]];
|
|
}}
|
|
formatMessageValue={({count, window}: RateLimitValue) =>
|
|
tct('[errors] in [timeWindow]', {
|
|
errors: tn('%s error ', '%s errors ', count),
|
|
timeWindow:
|
|
window === 0 ? t('no time window') : getExactDuration(window),
|
|
})
|
|
}
|
|
help={t(
|
|
'Apply a rate limit to this credential to cap the amount of errors accepted during a time window.'
|
|
)}
|
|
inline={false}
|
|
>
|
|
{({onChange, onBlur, value}) => {
|
|
const window = typeof value === 'object' ? value.window : undefined;
|
|
return (
|
|
<RateLimitRow>
|
|
<InputControl
|
|
type="number"
|
|
name="rateLimit.count"
|
|
min={0}
|
|
value={typeof value === 'object' ? value.count : undefined}
|
|
placeholder={t('Count')}
|
|
disabled={disabled || !hasFeature}
|
|
onChange={event => handleChangeCount(onChange, value, event)}
|
|
onBlur={event => handleChangeCount(onBlur, value, event)}
|
|
/>
|
|
<EventsIn>{t('event(s) in')}</EventsIn>
|
|
<RangeSlider
|
|
name="rateLimit.window"
|
|
allowedValues={getAllowedRateLimitValues(window)}
|
|
value={window}
|
|
placeholder={t('Window')}
|
|
formatLabel={rangeValue => {
|
|
if (typeof rangeValue === 'number') {
|
|
if (rangeValue === 0) {
|
|
return t('None');
|
|
}
|
|
return getExactDuration(rangeValue);
|
|
}
|
|
return undefined;
|
|
}}
|
|
disabled={disabled || !hasFeature}
|
|
onChange={(rangeValue, event) =>
|
|
handleChangeWindow(
|
|
onChange,
|
|
onBlur,
|
|
value,
|
|
Number(rangeValue),
|
|
event
|
|
)
|
|
}
|
|
/>
|
|
</RateLimitRow>
|
|
);
|
|
}}
|
|
</FormField>
|
|
</PanelBody>
|
|
</Panel>
|
|
)}
|
|
</Feature>
|
|
</Form>
|
|
);
|
|
}
|
|
|
|
export default KeyRateLimitsForm;
|
|
|
|
const RateLimitRow = styled('div')`
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr 2fr;
|
|
align-items: center;
|
|
gap: ${space(1)};
|
|
`;
|
|
|
|
const EventsIn = styled('small')`
|
|
font-size: ${p => p.theme.fontSizeRelativeSmall};
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
`;
|