organizationSampling.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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 {SegmentedControl} from 'sentry/components/segmentedControl';
  10. import {Tooltip} from 'sentry/components/tooltip';
  11. import {t, tct} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import {OnRouteLeave} from 'sentry/utils/reactRouter6Compat/onRouteLeave';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import {OrganizationSampleRateField} from 'sentry/views/settings/dynamicSampling/organizationSampleRateField';
  16. import {ProjectsPreviewTable} from 'sentry/views/settings/dynamicSampling/projectsPreviewTable';
  17. import {SamplingModeField} from 'sentry/views/settings/dynamicSampling/samplingModeField';
  18. import {organizationSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/organizationSamplingForm';
  19. import {
  20. type ProjectionSamplePeriod,
  21. useProjectSampleCounts,
  22. } from 'sentry/views/settings/dynamicSampling/utils/useProjectSampleCounts';
  23. import {useUpdateOrganization} from 'sentry/views/settings/dynamicSampling/utils/useUpdateOrganization';
  24. import {useAccess} from 'sentry/views/settings/projectMetrics/access';
  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} = useAccess({access: ['org:write']});
  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. <form onSubmit={event => event.preventDefault()}>
  62. <Panel>
  63. <PanelHeader>{t('Automatic Sampling')}</PanelHeader>
  64. <PanelBody>
  65. <SamplingModeField />
  66. <OrganizationSampleRateField />
  67. </PanelBody>
  68. </Panel>
  69. <OnRouteLeave
  70. message={UNSAVED_CHANGES_MESSAGE}
  71. when={locationChange =>
  72. locationChange.currentLocation.pathname !==
  73. locationChange.nextLocation.pathname && formState.hasChanged
  74. }
  75. />
  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. <Tooltip
  98. title={t(
  99. 'The time period for which the projected sample rates are calculated.'
  100. )}
  101. >
  102. <SegmentedControl value={period} onChange={setPeriod} size="xs">
  103. <SegmentedControl.Item key="24h">{t('24h')}</SegmentedControl.Item>
  104. <SegmentedControl.Item key="30d">{t('30d')}</SegmentedControl.Item>
  105. </SegmentedControl>
  106. </Tooltip>
  107. </HeadingRow>
  108. <p>
  109. {tct(
  110. 'This table gives you a preview of how your projects will be affected by the global sample rate. The [strong:projected rates are estimates] based on recent span volume.',
  111. {
  112. strong: <strong />,
  113. }
  114. )}
  115. </p>
  116. {sampleCountsQuery.isError ? (
  117. <LoadingError onRetry={sampleCountsQuery.refetch} />
  118. ) : (
  119. <ProjectsPreviewTable
  120. sampleCounts={sampleCountsQuery.data}
  121. isLoading={sampleCountsQuery.isPending}
  122. />
  123. )}
  124. <SubTextParagraph>
  125. {t('Inactive projects are not listed and will be sampled at 100% initially.')}
  126. </SubTextParagraph>
  127. </form>
  128. </FormProvider>
  129. );
  130. }
  131. const FormActions = styled('div')`
  132. display: grid;
  133. grid-template-columns: repeat(2, max-content);
  134. gap: ${space(1)};
  135. justify-content: flex-end;
  136. padding-bottom: ${space(4)};
  137. `;
  138. const HeadingRow = styled('div')`
  139. display: flex;
  140. align-items: center;
  141. justify-content: space-between;
  142. padding-bottom: ${space(1.5)};
  143. & > h4 {
  144. margin: 0;
  145. }
  146. `;
  147. const SubTextParagraph = styled('p')`
  148. color: ${p => p.theme.subText};
  149. font-size: ${p => p.theme.fontSizeSmall};
  150. `;