samplingModeSwitchModal.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. [prioritiesLink:Dynamic sampling priorities] continue to apply within the projects. [link:Learn more]',
  98. {
  99. prioritiesLink: (
  100. <ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
  101. ),
  102. link: (
  103. <ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
  104. ),
  105. }
  106. )}
  107. </p>
  108. {samplingMode === 'organization' && <TargetRateInput disabled={isPending} />}
  109. <p>
  110. {samplingMode === 'organization'
  111. ? tct(
  112. 'By switching [strong:you will lose your manually configured sample rates].',
  113. {
  114. strong: <strong />,
  115. }
  116. )
  117. : t('You can switch back to automatic mode at any time.')}
  118. </p>
  119. </Body>
  120. <Footer>
  121. <ButtonWrapper>
  122. <Button disabled={isPending} onClick={closeModal}>
  123. {t('Cancel')}
  124. </Button>
  125. <Button
  126. priority="primary"
  127. disabled={isPending || !formState.isValid}
  128. onClick={handleSubmit}
  129. >
  130. {t('Switch Mode')}
  131. </Button>
  132. </ButtonWrapper>
  133. </Footer>
  134. </form>
  135. </FormProvider>
  136. );
  137. }
  138. function TargetRateInput({disabled}: {disabled?: boolean}) {
  139. const id = useId();
  140. const {value, onChange, error} = useFormField('targetSampleRate');
  141. return (
  142. <FieldGroup
  143. label={t('Global Target Sample Rate')}
  144. css={{paddingBottom: space(0.5)}}
  145. inline={false}
  146. showHelpInTooltip
  147. flexibleControlStateSize
  148. stacked
  149. required
  150. >
  151. <InputWrapper>
  152. <PercentInput
  153. id={id}
  154. aria-label={t('Global Target Sample Rate')}
  155. value={value}
  156. onChange={event => onChange(event.target.value)}
  157. disabled={disabled}
  158. />
  159. <ErrorMessage>
  160. {error
  161. ? error
  162. : // Placholder character to keep the space occupied
  163. '\u200b'}
  164. </ErrorMessage>
  165. </InputWrapper>
  166. </FieldGroup>
  167. );
  168. }
  169. const InputWrapper = styled('div')`
  170. display: flex;
  171. flex-direction: column;
  172. gap: ${space(0.5)};
  173. `;
  174. const ErrorMessage = styled('div')`
  175. color: ${p => p.theme.red300};
  176. font-size: ${p => p.theme.fontSizeExtraSmall};
  177. `;
  178. const ButtonWrapper = styled('div')`
  179. display: flex;
  180. gap: ${space(2)};
  181. `;
  182. export function openSamplingModeSwitchModal(props: Props) {
  183. openModal(dialogProps => <SamplingModeSwitchModal {...dialogProps} {...props} />);
  184. }