projectsPreviewTable.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import Panel from 'sentry/components/panels/panel';
  4. import {t} from 'sentry/locale';
  5. import {space} from 'sentry/styles/space';
  6. import {useDebouncedValue} from 'sentry/utils/useDebouncedValue';
  7. import {OrganizationSampleRateInput} from 'sentry/views/settings/dynamicSampling/organizationSampleRateInput';
  8. import {ProjectsTable} from 'sentry/views/settings/dynamicSampling/projectsTable';
  9. import {SamplingBreakdown} from 'sentry/views/settings/dynamicSampling/samplingBreakdown';
  10. import {mapArrayToObject} from 'sentry/views/settings/dynamicSampling/utils';
  11. import {formatPercent} from 'sentry/views/settings/dynamicSampling/utils/formatPercent';
  12. import {organizationSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/organizationSamplingForm';
  13. import {parsePercent} from 'sentry/views/settings/dynamicSampling/utils/parsePercent';
  14. import {balanceSampleRate} from 'sentry/views/settings/dynamicSampling/utils/rebalancing';
  15. import type {
  16. ProjectionSamplePeriod,
  17. ProjectSampleCount,
  18. } from 'sentry/views/settings/dynamicSampling/utils/useProjectSampleCounts';
  19. const {useFormField} = organizationSamplingForm;
  20. interface Props {
  21. actions: React.ReactNode;
  22. isLoading: boolean;
  23. period: ProjectionSamplePeriod;
  24. sampleCounts: ProjectSampleCount[];
  25. }
  26. export function ProjectsPreviewTable({actions, isLoading, period, sampleCounts}: Props) {
  27. const sampleRateField = useFormField('targetSampleRate');
  28. const debouncedTargetSampleRate = useDebouncedValue(
  29. sampleRateField.value,
  30. // For longer lists we debounce the input to avoid too many re-renders
  31. sampleCounts.length > 100 ? 200 : 0
  32. );
  33. const balancingItems = useMemo(
  34. () =>
  35. sampleCounts.map(item => ({
  36. ...item,
  37. // Add properties to match the BalancingItem type of the balanceSampleRate function
  38. id: item.project.id,
  39. sampleRate: 1,
  40. })),
  41. [sampleCounts]
  42. );
  43. const {balancedItems} = useMemo(() => {
  44. const targetRate = parsePercent(debouncedTargetSampleRate);
  45. return balanceSampleRate({
  46. targetSampleRate: targetRate,
  47. items: balancingItems,
  48. });
  49. }, [debouncedTargetSampleRate, balancingItems]);
  50. const initialSampleRatesById = useMemo(() => {
  51. const targetRate = parsePercent(sampleRateField.initialValue);
  52. const {balancedItems: initialBalancedItems} = balanceSampleRate({
  53. targetSampleRate: targetRate,
  54. items: balancingItems,
  55. });
  56. return mapArrayToObject({
  57. array: initialBalancedItems,
  58. keySelector: item => item.id,
  59. valueSelector: item => item.sampleRate,
  60. });
  61. }, [sampleRateField.initialValue, balancingItems]);
  62. const itemsWithFormattedNumbers = useMemo(() => {
  63. return balancedItems.map(item => ({
  64. ...item,
  65. sampleRate: formatPercent(item.sampleRate),
  66. initialSampleRate: formatPercent(initialSampleRatesById[item.id]!),
  67. }));
  68. }, [balancedItems, initialSampleRatesById]);
  69. const breakdownSampleRates = useMemo(
  70. () =>
  71. mapArrayToObject({
  72. array: balancedItems,
  73. keySelector: item => item.id,
  74. valueSelector: item => item.sampleRate,
  75. }),
  76. [balancedItems]
  77. );
  78. return (
  79. <Fragment>
  80. <SamplingBreakdown
  81. sampleCounts={sampleCounts}
  82. sampleRates={breakdownSampleRates}
  83. isLoading={isLoading}
  84. />
  85. <Panel>
  86. <OrganizationSampleRateInput
  87. value={sampleRateField.value}
  88. onChange={sampleRateField.onChange}
  89. previousValue={sampleRateField.initialValue}
  90. showPreviousValue={sampleRateField.hasChanged}
  91. error={sampleRateField.error}
  92. label={t('Target Sample Rate')}
  93. help={t(
  94. 'Set a global sample rate for your entire organization. This will determine how much incoming traffic should be stored across all your projects.'
  95. )}
  96. />
  97. <ProjectsTable
  98. rateHeader={t('Target Rate')}
  99. canEdit={false}
  100. emptyMessage={t('No active projects found in the selected period.')}
  101. period={period}
  102. isLoading={isLoading}
  103. items={itemsWithFormattedNumbers}
  104. />
  105. <Footer>{actions}</Footer>
  106. </Panel>
  107. </Fragment>
  108. );
  109. }
  110. const Footer = styled('div')`
  111. border-top: 1px solid ${p => p.theme.innerBorder};
  112. display: flex;
  113. justify-content: flex-end;
  114. gap: ${space(2)};
  115. padding: ${space(1.5)} ${space(2)};
  116. `;