organizationSampling.tsx 5.4 KB

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