samplingModeSwitchModal.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {useId} from 'react';
  2. import styled from '@emotion/styled';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import {type ModalRenderProps, openModal} from 'sentry/actionCreators/modal';
  9. import {Button} from 'sentry/components/button';
  10. import FieldGroup from 'sentry/components/forms/fieldGroup';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import {t, tct} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {Organization} from 'sentry/types/organization';
  15. import {PercentInput} from 'sentry/views/settings/dynamicSampling/percentInput';
  16. import {formatPercent} from 'sentry/views/settings/dynamicSampling/utils/formatPercent';
  17. import {organizationSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/organizationSamplingForm';
  18. import {parsePercent} from 'sentry/views/settings/dynamicSampling/utils/parsePercent';
  19. import {useUpdateOrganization} from 'sentry/views/settings/dynamicSampling/utils/useUpdateOrganization';
  20. interface Props {
  21. /**
  22. * The sampling mode to switch to.
  23. */
  24. samplingMode: Organization['samplingMode'];
  25. /**
  26. * The initial target rate for the automatic sampling mode.
  27. * Required if `samplingMode` is 'automatic'.
  28. */
  29. initialTargetRate?: number;
  30. }
  31. const {FormProvider, useFormState, useFormField} = organizationSamplingForm;
  32. function SamplingModeSwitchModal({
  33. Header,
  34. Body,
  35. Footer,
  36. closeModal,
  37. samplingMode,
  38. initialTargetRate = 1,
  39. }: Props & ModalRenderProps) {
  40. const formState = useFormState({
  41. initialValues: {
  42. targetSampleRate: formatPercent(initialTargetRate),
  43. },
  44. });
  45. const {mutate: updateOrganization, isPending} = useUpdateOrganization({
  46. onMutate: () => {
  47. addLoadingMessage(t('Switching sampling mode...'));
  48. },
  49. onSuccess: () => {
  50. addSuccessMessage(t('Changes applied.'));
  51. closeModal();
  52. },
  53. onError: () => {
  54. addErrorMessage(t('Unable to save changes. Please try again.'));
  55. },
  56. });
  57. const handleSubmit = () => {
  58. if (!formState.isValid) {
  59. return;
  60. }
  61. const changes: Parameters<typeof updateOrganization>[0] = {
  62. samplingMode,
  63. };
  64. if (samplingMode === 'organization') {
  65. changes.targetSampleRate = parsePercent(formState.fields.targetSampleRate.value);
  66. }
  67. updateOrganization(changes);
  68. };
  69. return (
  70. <FormProvider formState={formState}>
  71. <form
  72. onSubmit={event => {
  73. event.preventDefault();
  74. handleSubmit();
  75. }}
  76. noValidate
  77. >
  78. <Header>
  79. <h5>
  80. {samplingMode === 'organization'
  81. ? t('Switch to Automatic Mode')
  82. : t('Switch to Manual Mode')}
  83. </h5>
  84. </Header>
  85. <Body>
  86. <p>
  87. {samplingMode === 'organization'
  88. ? tct(
  89. 'Switching to automatic mode enables continuous adjustments for your projects based on a global target sample rate. Sentry boosts the sample rates of small projects and ensures equal visibility. [link:Learn more]',
  90. {
  91. link: (
  92. <ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
  93. ),
  94. }
  95. )
  96. : tct(
  97. 'Switching to manual mode disables automatic adjustments. After the switch, you can configure individual sample rates for each project. Dynamic sampling priorities continue to apply within the projects. [link:Learn more]',
  98. {
  99. link: (
  100. <ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
  101. ),
  102. }
  103. )}
  104. </p>
  105. {samplingMode === 'organization' && <TargetRateInput disabled={isPending} />}
  106. <p>
  107. {samplingMode === 'organization'
  108. ? tct(
  109. 'By switching [strong:you will lose your manually configured sample rates].',
  110. {
  111. strong: <strong />,
  112. }
  113. )
  114. : t('You can switch back to automatic mode at any time.')}
  115. </p>
  116. </Body>
  117. <Footer>
  118. <ButtonWrapper>
  119. <Button disabled={isPending} onClick={closeModal}>
  120. {t('Cancel')}
  121. </Button>
  122. <Button
  123. priority="primary"
  124. disabled={isPending || !formState.isValid}
  125. onClick={handleSubmit}
  126. >
  127. {t('Switch Mode')}
  128. </Button>
  129. </ButtonWrapper>
  130. </Footer>
  131. </form>
  132. </FormProvider>
  133. );
  134. }
  135. function TargetRateInput({disabled}: {disabled?: boolean}) {
  136. const id = useId();
  137. const {value, onChange, error} = useFormField('targetSampleRate');
  138. return (
  139. <FieldGroup
  140. label={t('Global Target Sample Rate')}
  141. css={{paddingBottom: space(0.5)}}
  142. inline={false}
  143. showHelpInTooltip
  144. flexibleControlStateSize
  145. stacked
  146. required
  147. >
  148. <InputWrapper>
  149. <PercentInput
  150. id={id}
  151. aria-label={t('Global Target Sample Rate')}
  152. value={value}
  153. onChange={event => onChange(event.target.value)}
  154. disabled={disabled}
  155. />
  156. <ErrorMessage>
  157. {error
  158. ? error
  159. : // Placholder character to keep the space occupied
  160. '\u200b'}
  161. </ErrorMessage>
  162. </InputWrapper>
  163. </FieldGroup>
  164. );
  165. }
  166. const InputWrapper = styled('div')`
  167. display: flex;
  168. flex-direction: column;
  169. gap: ${space(0.5)};
  170. `;
  171. const ErrorMessage = styled('div')`
  172. color: ${p => p.theme.red300};
  173. font-size: ${p => p.theme.fontSizeExtraSmall};
  174. `;
  175. const ButtonWrapper = styled('div')`
  176. display: flex;
  177. gap: ${space(2)};
  178. `;
  179. export function openSamplingModeSwitchModal(props: Props) {
  180. openModal(dialogProps => <SamplingModeSwitchModal {...dialogProps} {...props} />);
  181. }