organizationSampling.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import {useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {Button} from 'sentry/components/button';
  5. import LoadingError from 'sentry/components/loadingError';
  6. import Panel from 'sentry/components/panels/panel';
  7. import PanelBody from 'sentry/components/panels/panelBody';
  8. import PanelHeader from 'sentry/components/panels/panelHeader';
  9. import {Tooltip} from 'sentry/components/tooltip';
  10. import {t, tct} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import {OnRouteLeave} from 'sentry/utils/reactRouter6Compat/onRouteLeave';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import {OrganizationSampleRateField} from 'sentry/views/settings/dynamicSampling/organizationSampleRateField';
  15. import {ProjectionPeriodControl} from 'sentry/views/settings/dynamicSampling/projectionPeriodControl';
  16. import {ProjectsPreviewTable} from 'sentry/views/settings/dynamicSampling/projectsPreviewTable';
  17. import {SamplingModeField} from 'sentry/views/settings/dynamicSampling/samplingModeField';
  18. import {useHasDynamicSamplingWriteAccess} from 'sentry/views/settings/dynamicSampling/utils/access';
  19. import {organizationSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/organizationSamplingForm';
  20. import {parsePercent} from 'sentry/views/settings/dynamicSampling/utils/parsePercent';
  21. import {
  22. type ProjectionSamplePeriod,
  23. useProjectSampleCounts,
  24. } from 'sentry/views/settings/dynamicSampling/utils/useProjectSampleCounts';
  25. import {useUpdateOrganization} from 'sentry/views/settings/dynamicSampling/utils/useUpdateOrganization';
  26. const {useFormState, FormProvider} = organizationSamplingForm;
  27. const UNSAVED_CHANGES_MESSAGE = t(
  28. 'You have unsaved changes, are you sure you want to leave?'
  29. );
  30. export function OrganizationSampling() {
  31. const organization = useOrganization();
  32. const hasAccess = useHasDynamicSamplingWriteAccess();
  33. const [period, setPeriod] = useState<ProjectionSamplePeriod>('24h');
  34. const formState = useFormState({
  35. initialValues: {
  36. targetSampleRate: ((organization.targetSampleRate ?? 1) * 100)?.toLocaleString(),
  37. },
  38. });
  39. const sampleCountsQuery = useProjectSampleCounts({period});
  40. const {mutate: updateOrganization, isPending} = useUpdateOrganization();
  41. const handleSubmit = () => {
  42. updateOrganization(
  43. {
  44. targetSampleRate: parsePercent(formState.fields.targetSampleRate.value),
  45. },
  46. {
  47. onSuccess: () => {
  48. addSuccessMessage(t('Changes applied.'));
  49. formState.save();
  50. },
  51. onError: () => {
  52. addErrorMessage(t('Unable to save changes. Please try again.'));
  53. },
  54. }
  55. );
  56. };
  57. const handleReset = () => {
  58. formState.reset();
  59. };
  60. return (
  61. <FormProvider formState={formState}>
  62. <OnRouteLeave
  63. message={UNSAVED_CHANGES_MESSAGE}
  64. when={locationChange =>
  65. locationChange.currentLocation.pathname !==
  66. locationChange.nextLocation.pathname && formState.hasChanged
  67. }
  68. />
  69. <form onSubmit={event => event.preventDefault()} noValidate>
  70. <Panel>
  71. <PanelHeader>{t('General Settings')}</PanelHeader>
  72. <PanelBody>
  73. <SamplingModeField />
  74. <OrganizationSampleRateField />
  75. </PanelBody>
  76. </Panel>
  77. <FormActions>
  78. <Button disabled={!formState.hasChanged || isPending} onClick={handleReset}>
  79. {t('Reset')}
  80. </Button>
  81. <Tooltip
  82. disabled={hasAccess}
  83. title={t('You do not have permission to update these settings.')}
  84. >
  85. <Button
  86. priority="primary"
  87. disabled={
  88. !hasAccess || !formState.isValid || !formState.hasChanged || isPending
  89. }
  90. onClick={handleSubmit}
  91. >
  92. {t('Save changes')}
  93. </Button>
  94. </Tooltip>
  95. </FormActions>
  96. <HeadingRow>
  97. <h4>{t('Project Preview')}</h4>
  98. <ProjectionPeriodControl period={period} onChange={setPeriod} />
  99. </HeadingRow>
  100. <p>
  101. {tct(
  102. 'This table gives you a preview of how your projects will be affected by the target sample rate. The [strong:estimated rates] are based on recent span volume and change continuously.',
  103. {
  104. strong: <strong />,
  105. }
  106. )}
  107. </p>
  108. <p>
  109. {t(
  110. 'Rates apply to all spans in traces that start in each project, including a portion of spans in connected other projects.'
  111. )}
  112. </p>
  113. {sampleCountsQuery.isError ? (
  114. <LoadingError onRetry={sampleCountsQuery.refetch} />
  115. ) : (
  116. <ProjectsPreviewTable
  117. sampleCounts={sampleCountsQuery.data}
  118. isLoading={sampleCountsQuery.isPending}
  119. />
  120. )}
  121. <SubTextParagraph>
  122. {t('Inactive projects are not listed and will be sampled at 100% initially.')}
  123. </SubTextParagraph>
  124. </form>
  125. </FormProvider>
  126. );
  127. }
  128. const FormActions = styled('div')`
  129. display: grid;
  130. grid-template-columns: repeat(2, max-content);
  131. gap: ${space(1)};
  132. justify-content: flex-end;
  133. padding-bottom: ${space(4)};
  134. `;
  135. const HeadingRow = styled('div')`
  136. display: flex;
  137. align-items: center;
  138. justify-content: space-between;
  139. padding-bottom: ${space(1.5)};
  140. & > h4 {
  141. margin: 0;
  142. }
  143. `;
  144. const SubTextParagraph = styled('p')`
  145. color: ${p => p.theme.subText};
  146. font-size: ${p => p.theme.fontSizeSmall};
  147. `;