samplingModeSwitchModal.tsx 6.2 KB

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