projectSampling.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import {Fragment, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import {Button} from 'sentry/components/button';
  9. import LoadingError from 'sentry/components/loadingError';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import {OnRouteLeave} from 'sentry/utils/reactRouter6Compat/onRouteLeave';
  13. import {ProjectionPeriodControl} from 'sentry/views/settings/dynamicSampling/projectionPeriodControl';
  14. import {ProjectsEditTable} from 'sentry/views/settings/dynamicSampling/projectsEditTable';
  15. import {SamplingModeSwitch} from 'sentry/views/settings/dynamicSampling/samplingModeSwitch';
  16. import {mapArrayToObject} from 'sentry/views/settings/dynamicSampling/utils';
  17. import {useHasDynamicSamplingWriteAccess} from 'sentry/views/settings/dynamicSampling/utils/access';
  18. import {parsePercent} from 'sentry/views/settings/dynamicSampling/utils/parsePercent';
  19. import {projectSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/projectSamplingForm';
  20. import {
  21. type ProjectionSamplePeriod,
  22. useProjectSampleCounts,
  23. } from 'sentry/views/settings/dynamicSampling/utils/useProjectSampleCounts';
  24. import {
  25. useGetSamplingProjectRates,
  26. useUpdateSamplingProjectRates,
  27. } from 'sentry/views/settings/dynamicSampling/utils/useSamplingProjectRates';
  28. const {useFormState, FormProvider} = projectSamplingForm;
  29. const UNSAVED_CHANGES_MESSAGE = t(
  30. 'You have unsaved changes, are you sure you want to leave?'
  31. );
  32. export function ProjectSampling() {
  33. const hasAccess = useHasDynamicSamplingWriteAccess();
  34. const [period, setPeriod] = useState<ProjectionSamplePeriod>('24h');
  35. const [editMode, setEditMode] = useState<'single' | 'bulk'>('single');
  36. const sampleRatesQuery = useGetSamplingProjectRates();
  37. const sampleCountsQuery = useProjectSampleCounts({period});
  38. const updateSamplingProjectRates = useUpdateSamplingProjectRates();
  39. const projectRates = useMemo(
  40. () =>
  41. (sampleRatesQuery.data || []).reduce(
  42. (acc, item) => {
  43. acc[item.id.toString()] = (item.sampleRate * 100).toString();
  44. return acc;
  45. },
  46. {} as Record<string, string>
  47. ),
  48. [sampleRatesQuery.data]
  49. );
  50. const initialValues = useMemo(() => ({projectRates}), [projectRates]);
  51. const formState = useFormState({
  52. initialValues,
  53. enableReInitialize: true,
  54. });
  55. const handleReset = () => {
  56. formState.reset();
  57. setEditMode('single');
  58. };
  59. const handleSubmit = () => {
  60. const ratesArray = Object.entries(formState.fields.projectRates.value).map(
  61. ([id, rate]) => ({
  62. id: Number(id),
  63. sampleRate: parsePercent(rate),
  64. })
  65. );
  66. addLoadingMessage(t('Saving changes...'));
  67. updateSamplingProjectRates.mutate(ratesArray, {
  68. onSuccess: () => {
  69. formState.save();
  70. setEditMode('single');
  71. addSuccessMessage(t('Changes applied'));
  72. },
  73. onError: () => {
  74. addErrorMessage(t('Unable to save changes. Please try again.'));
  75. },
  76. });
  77. };
  78. const initialTargetRate = useMemo(() => {
  79. const sampleRates = sampleRatesQuery.data ?? [];
  80. const spanCounts = sampleCountsQuery.data ?? [];
  81. const totalSpanCount = spanCounts.reduce((acc, item) => acc + item.count, 0);
  82. const spanCountsById = mapArrayToObject({
  83. array: spanCounts,
  84. keySelector: item => item.project.id,
  85. valueSelector: item => item.count,
  86. });
  87. return (
  88. sampleRates.reduce((acc, item) => {
  89. const count = spanCountsById[item.id] ?? 0;
  90. return acc + count * item.sampleRate;
  91. }, 0) / totalSpanCount
  92. );
  93. }, [sampleRatesQuery.data, sampleCountsQuery.data]);
  94. const isFormActionDisabled =
  95. !hasAccess ||
  96. sampleRatesQuery.isPending ||
  97. updateSamplingProjectRates.isPending ||
  98. !formState.hasChanged;
  99. return (
  100. <FormProvider formState={formState}>
  101. <OnRouteLeave
  102. message={UNSAVED_CHANGES_MESSAGE}
  103. when={locationChange =>
  104. locationChange.currentLocation.pathname !==
  105. locationChange.nextLocation.pathname && formState.hasChanged
  106. }
  107. />
  108. <MainControlBar>
  109. <ProjectionPeriodControl period={period} onChange={setPeriod} />
  110. <SamplingModeSwitch initialTargetRate={initialTargetRate} />
  111. </MainControlBar>
  112. {sampleCountsQuery.isError ? (
  113. <LoadingError onRetry={sampleCountsQuery.refetch} />
  114. ) : (
  115. <ProjectsEditTable
  116. period={period}
  117. editMode={editMode}
  118. onEditModeChange={setEditMode}
  119. isLoading={sampleRatesQuery.isPending || sampleCountsQuery.isPending}
  120. sampleCounts={sampleCountsQuery.data}
  121. actions={
  122. <Fragment>
  123. <Button disabled={isFormActionDisabled} onClick={handleReset}>
  124. {t('Reset')}
  125. </Button>
  126. <Button
  127. priority="primary"
  128. disabled={isFormActionDisabled || !formState.isValid}
  129. onClick={handleSubmit}
  130. >
  131. {t('Apply Changes')}
  132. </Button>
  133. </Fragment>
  134. }
  135. />
  136. )}
  137. <FormActions />
  138. </FormProvider>
  139. );
  140. }
  141. const MainControlBar = styled('div')`
  142. display: flex;
  143. justify-content: space-between;
  144. margin-bottom: ${space(1.5)};
  145. `;
  146. const FormActions = styled('div')`
  147. display: grid;
  148. grid-template-columns: repeat(2, max-content);
  149. gap: ${space(1)};
  150. justify-content: flex-end;
  151. padding-bottom: ${space(4)};
  152. `;